全部產品
Search
文件中心

Application Real-Time Monitoring Service:ECS上通過OpenTelemetry eBPF(OBI)接入可觀測

更新時間:Jul 02, 2026

通過OpenTelemetry eBPF Instrumentation(OBI)提供的無侵入監控能力上報鏈路資料至ARMS後,ARMS即可開始監控應用,您可以查看應用拓撲、調用鏈路、異常事務、慢事務和SQL分析等一系列監控資料。本文介紹如何使用OBI為應用進行監控並上報資料。

前提條件

說明

如果您在使用OpenTelemetry eBPF過程中有任何問題,歡迎通過DingTalk答疑群(群號:159215000379)與我們聯絡。

  • 已準備一台滿足以下條件的 Linux 伺服器(Elastic Compute Service 或本地環境均可)。

    要求

    支援範圍

    核心版本

    Linux 5.8 及以上。RHEL 系發行版(帶有 eBPF 回移補丁)最低支援 Linux 4.18。

    BTF

    核心必須暴露 BTF(BPF Type Format)資訊。

    CPU 架構

    amd64(x86_64)、arm64(aarch64)。

    許可權

    Root 許可權。

    說明

    阿里雲 Alibaba Cloud Linux 3 和 Alibaba Cloud Linux 4 預設滿足以上要求。

    如果需要確認伺服器是否滿足條件,運行以下命令逐項檢查:

    # 查看核心版本(需 >= 5.8,RHEL 系 >= 4.18)
    uname -r
    
    # 查看 CPU 架構(需為 amd64 或 arm64)
    uname -m
    
    # 檢查 BTF 是否可用(檔案存在即支援)
    ls /sys/kernel/btf/vmlinux
    
    # 檢查是否有 root 許可權
    whoami

背景資訊

OBI(OpenTelemetry eBPF Instrumentation)是一款基於 eBPF 技術的 Kubernetes 可觀測性自動插樁組件。它以 DaemonSet 方式部署在叢集節點上,無需修改應用代碼或注入 Sidecar,即可自動採集應用的 Metrics 和 Traces 資料。OBI 支援 HTTP、gRPC、SQL、Redis、Kafka 等主流協議的自動探測,並針對 GenAI 情境(OpenAI、Anthropic、通義千問、MCP 、Embedding、Rerank等)提供了請求/響應體的深度提取能力。採集的遙測資料通過 OTLP 協議匯出,可無縫對接 ARMS 等可觀測性平台,協助使用者實現零侵入、低開銷的全鏈路可觀測。

應用發現匹配方式

OBI 通過 discovery.instrument 配置段發現和匹配要監控的進程,支援以下匹配方式(所有條件為 AND 邏輯,全部滿足才會匹配):

YAML 欄位

類型

說明

樣本

open_ports

連接埠號碼/範圍

進程監聽的連接埠號碼,支援逗號分隔和範圍

808080,4438000-8999

exe_path

Glob 萬用字元

匹配進程可執行檔完整路徑

*/java*/nginx*myapp*

cmd_args

Glob 萬用字元

匹配進程命令列參數(不含可執行檔名)

*petclinic**--config=prod*

languages

Glob 萬用字元

按程式設計語言自動匹配(go/java/python/ruby/nodejs/rust/dotnet/php/cpp)

java{go,rust}

target_pids

PID 列表

直接指定進程 PID

[12345, 67890]

container_name

Glob 萬用字元

匹配 OCI 容器名稱(適用於 Docker 部署)

my-container-*

containers_only

布爾值

僅匹配容器內進程

true

name

字串

在ARMS控制台顯示的應用程式名稱

my-app

namespace

字串

服務命名空間

production

exports

訊號列表

控制匯出的訊號類型(metrics/traces/logs),空數組關閉全部

[metrics, traces]

sampler

採樣配置

按比例採樣 Traces,減少資料量

name: traceidratio, arg: "0.5"

常見情境配置樣本

情境1:按連接埠號碼匹配 — 最簡單,適用於任何類型的 Web 應用程式

discovery:
  instrument:
    - name: my-web-app
      open_ports: 8080

情境2:Java 應用(exe_path + cmd_args) — 通過 java 進程 + jar 包名定位

discovery:
  instrument:
    - name: my-java-app
      exe_path: "*/java"
      cmd_args: "*petclinic*"

情境3:按程式設計語言自動探索 — 自動檢測指定語言的所有進程

discovery:
  instrument:
    - name: go-services
      languages: go
    - name: python-services
      languages: "{python,nodejs}"

情境4:按可執行檔路徑匹配 — 適用於自編譯的二進位

discovery:
  instrument:
    - name: my-binary
      exe_path: "*/my-server"

情境5:Docker 容器名匹配 — 適用於 ECS 上跑 Docker 的情境

discovery:
  instrument:
    - name: my-container-app
      container_name: "my-container-*"

情境6:多連接埠範圍 — 監控一批連接埠範圍內的服務

discovery:
  instrument:
    - name: microservices
      open_ports: 8080-8090,9090

情境7:指定 PID 臨時調試 — 直接指定進程 PID 進行監控

discovery:
  instrument:
    - name: debug-target
      target_pids: [12345]

情境8:僅容器進程 + 採樣 — 只監控容器內進程且對 Traces 採樣 50%

discovery:
  instrument:
    - name: container-apps
      containers_only: true
      open_ports: 8080
      sampler:
        name: traceidratio
        arg: "0.5"

情境9:多應用 + 排除規則 — 監控多個應用,排除特定進程

discovery:
  instrument:
    - name: api-gateway
      open_ports: 80,443
    - name: backend-java
      exe_path: "*/java"
      cmd_args: "*backend*"
    - name: worker
      exe_path: "*/python*"
      cmd_args: "*celery*"
  exclude_instrument:
    - exe_path: "{obi,prometheus,otelcol*}"

情境10:控制匯出訊號 — 只匯出 Metrics 不匯出 Traces

discovery:
  instrument:
    - name: high-qps-app
      open_ports: 8080
      exports: [metrics]

操作步驟

操作流程總覽:安裝部署(擷取存取點資訊 → 下載 OBI → 編寫設定檔 → 部署服務) → 驗證監控資料

安裝部署

提供一鍵指令碼部署(推薦)和手動分步部署方式。

方式一:一鍵指令碼部署(推薦)

已將所有步驟合并為一個 obi-setup.sh 指令碼,支援互動式和非互動式兩種模式。

#!/bin/bash
# ============================================================================
#  OBI (OpenTelemetry eBPF Instrumentation) 一鍵部署指令碼
#  合并了環境檢查、下載、配置產生、服務安裝的完整流程
#
#  用法:
#    sudo bash obi-setup.sh install   — 互動式安裝(二進位 + systemd)
#    sudo bash obi-setup.sh docker    — Docker 方式部署
#    sudo bash obi-setup.sh check     — 僅做環境檢查
#    sudo bash obi-setup.sh uninstall — 卸載 OBI 服務
#    sudo bash obi-setup.sh status    — 查看運行狀態與日誌
#
#  非互動式部署(環境變數):
#    export OTLP_ENDPOINT="http://xxx:4318"
#    export OTLP_HEADERS="Authentication=xxx"
#    export APP_NAME="my-app"
#    export APP_MATCH_TYPE="port"    # port|cmd|exe|lang|container|pid
#    export APP_MATCH_VALUE="8080"
#    sudo -E bash obi-setup.sh install
# ============================================================================

set -euo pipefail

# ========================= 全域變數 =========================
VERSION="0.9.5"
SERVICE_NAME="obi"
INSTALL_DIR="/opt/obi"
CONFIG_FILE="${INSTALL_DIR}/config.yaml"
OBI_BINARY="${INSTALL_DIR}/obi"
DOCKER_IMAGE="registry.cn-hangzhou.aliyuncs.com/private-mesh/obi:${VERSION}"
OSS_BASE="https://arms-apm-cn-hangzhou-pre.oss-cn-hangzhou.aliyuncs.com/obi-agent/${VERSION}"

# 顏色
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

# 應用列表(數組)
declare -a APP_ENTRIES=()
# 排除規則
EXCLUDE_PATTERN=""

# ========================= 工具函數 =========================
info()  { echo -e "${CYAN}[INFO]${NC} $*"; }
ok()    { echo -e "${GREEN}[OK]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
fail()  { echo -e "${RED}[FAIL]${NC} $*"; }
die()   { fail "$*"; exit 1; }

prompt_input() {
    local var_name="$1" prompt_msg="$2" default_val="${3:-}"
    local current_val="${!var_name:-}"
    if [[ -n "$current_val" ]]; then
        info "使用環境變數 ${var_name}=${current_val}"
        return
    fi
    if [[ -n "$default_val" ]]; then
        read -rp "$(echo -e "${BOLD}${prompt_msg} [${default_val}]: ${NC}")" input
        eval "$var_name=\"${input:-$default_val}\""
    else
        while true; do
            read -rp "$(echo -e "${BOLD}${prompt_msg}: ${NC}")" input
            if [[ -n "$input" ]]; then
                eval "$var_name=\"$input\""
                return
            fi
            warn "此項為必填"
        done
    fi
}

prompt_yesno() {
    local prompt_msg="$1" default="${2:-n}"
    local yn
    read -rp "$(echo -e "${BOLD}${prompt_msg} [y/N]: ${NC}")" yn
    case "${yn:-$default}" in
        [Yy]*) return 0 ;;
        *) return 1 ;;
    esac
}

# ========================= 環境檢查 =========================
check_root() {
    if [[ "$EUID" -ne 0 ]]; then
        die "請使用 root 許可權運行: sudo bash $0 $*"
    fi
    ok "Root 許可權"
}

check_env() {
    echo ""
    echo -e "${BOLD}========== 環境檢查 ==========${NC}"

    local kernel_ver major minor
    kernel_ver=$(uname -r)
    major=$(echo "$kernel_ver" | cut -d. -f1)
    minor=$(echo "$kernel_ver" | cut -d. -f2)

    local is_rhel=false
    if [[ -f /etc/redhat-release ]] || grep -qi 'rhel\|centos\|rocky\|alma\|anolis\|alinux' /etc/os-release 2>/dev/null; then
        is_rhel=true
    fi

    if [[ "$major" -gt 5 ]] || { [[ "$major" -eq 5 ]] && [[ "$minor" -ge 8 ]]; }; then
        ok "核心版本: ${kernel_ver} (>= 5.8)"
    elif $is_rhel && { [[ "$major" -gt 4 ]] || { [[ "$major" -eq 4 ]] && [[ "$minor" -ge 18 ]]; }; }; then
        ok "核心版本: ${kernel_ver} (RHEL 系, >= 4.18)"
    else
        fail "核心版本: ${kernel_ver} (最低要求 5.8, RHEL 系 4.18)"
        return 1
    fi

    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) ok "CPU 架構: ${arch} (amd64)" ;;
        aarch64|arm64) ok "CPU 架構: ${arch} (arm64)" ;;
        *) fail "CPU 架構: ${arch} (不支援)"; return 1 ;;
    esac

    if [[ -f /sys/kernel/btf/vmlinux ]]; then
        ok "BTF: 可用"
    else
        fail "BTF: /sys/kernel/btf/vmlinux 不存在"
        return 1
    fi

    echo -e "${GREEN}========== 環境檢查通過 ==========${NC}"
    echo ""
    return 0
}

detect_arch() {
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) echo "amd64" ;;
        aarch64|arm64) echo "arm64" ;;
        *) die "不支援的 CPU 架構: ${arch}" ;;
    esac
}

# ========================= 單個應用配置收集 =========================
collect_one_app() {
    local app_name match_type
    local entry=""

    prompt_input app_name "應用程式名稱 (ARMS 控制台顯示名稱)" "my-app"

    echo ""
    echo -e "  ${BOLD}選擇應用匹配方式:${NC}"
    echo -e "    1) 連接埠號碼匹配        — open_ports, 如 8080 或 80,443 或 8000-8999"
    echo -e "    2) 命令列匹配        — exe_path + cmd_args, 適合 java -jar 等情境"
    echo -e "    3) 可執行檔路徑匹配 — exe_path, 適合自編譯二進位"
    echo -e "    4) 程式設計語言自動探索   — languages, 自動檢測 go/java/python/nodejs 等"
    echo -e "    5) Docker 容器名匹配  — container_name, 適合 ECS 上跑 Docker"
    echo -e "    6) PID 指定           — target_pids, 臨時調試用"
    read -rp "$(echo -e "${BOLD}選擇 [1-6, 預設1]: ${NC}")" match_choice

    entry="    - name: ${app_name}"

    case "${match_choice:-1}" in
        1)
            local ports
            prompt_input ports "監聽連接埠號碼 (如 8080, 80,443, 8000-8999)" "8080"
            entry="${entry}
      open_ports: ${ports}"
            ;;
        2)
            local exe_path cmd_args
            prompt_input exe_path "可執行檔路徑 Glob (如 */java)" "*/java"
            prompt_input cmd_args "命令列參數 Glob (如 *petclinic*)"
            entry="${entry}
      exe_path: \"${exe_path}\"
      cmd_args: \"${cmd_args}\""
            ;;
        3)
            local exe_path
            prompt_input exe_path "可執行檔路徑 Glob (如 */my-server, */nginx)"
            entry="${entry}
      exe_path: \"${exe_path}\""
            ;;
        4)
            local lang
            echo -e "    支援的語言: go, java, python, ruby, nodejs, rust, dotnet, php, cpp"
            echo -e "    多語言用花括弧: {go,rust} 或 {python,nodejs}"
            prompt_input lang "程式設計語言" "java"
            entry="${entry}
      languages: \"${lang}\""
            ;;
        5)
            local cname
            prompt_input cname "Docker 容器名 Glob (如 my-container-*)"
            entry="${entry}
      container_name: \"${cname}\""
            ;;
        6)
            local pids
            prompt_input pids "PID 列表, 逗號分隔 (如 12345,67890)"
            local pid_array
            pid_array=$(echo "$pids" | tr ',' '\n' | sed 's/^[[:space:]]*//' | awk '{printf "      - %s\n", $0}')
            entry="${entry}
      target_pids:
${pid_array}"
            ;;
        *)
            local ports
            prompt_input ports "監聽連接埠號碼" "8080"
            entry="${entry}
      open_ports: ${ports}"
            ;;
    esac

    # 可選:僅容器進程
    if [[ "${match_choice:-1}" != "5" ]]; then
        if prompt_yesno "是否僅匹配容器內進程 (containers_only)?"; then
            entry="${entry}
      containers_only: true"
        fi
    fi

    # 可選:採樣
    if prompt_yesno "是否配置 Traces 採樣 (減少資料量)?"; then
        echo -e "    採樣器: always_on, always_off, traceidratio, parentbased_traceidratio"
        local sampler_name sampler_arg
        prompt_input sampler_name "採樣器名稱" "traceidratio"
        prompt_input sampler_arg  "採樣參數 (如 0.5 表示採樣50%)" "1.0"
        entry="${entry}
      sampler:
        name: ${sampler_name}
        arg: \"${sampler_arg}\""
    fi

    APP_ENTRIES+=("$entry")
    ok "已添加應用: ${app_name}"
}

# ========================= 收集配置(完整) =========================
collect_config() {
    echo -e "${BOLD}========== 配置資訊 ==========${NC}"
    echo ""
    info "請從 ARMS 接入中心擷取以下資訊:"
    info "  ARMS 控制台 -> 接入中心 -> OpenTelemetry 卡片 -> 點擊擷取 LicenseKey"
    echo ""

    prompt_input OTLP_ENDPOINT "OTEL_EXPORTER_OTLP_ENDPOINT (如 http://xxx:4318)"
    prompt_input OTLP_HEADERS  "OTEL_EXPORTER_OTLP_HEADERS  (如 Authentication=xxx)"

    echo ""
    echo -e "${BOLD}========== 應用配置 ==========${NC}"

    # 非互動模式:從環境變數構建單個 app
    if [[ -n "${APP_MATCH_TYPE:-}" ]]; then
        local entry="    - name: ${APP_NAME:-my-app}"
        case "$APP_MATCH_TYPE" in
            port)
                entry="${entry}
      open_ports: ${APP_MATCH_VALUE:-8080}" ;;
            cmd)
                entry="${entry}
      exe_path: \"${APP_EXE_PATH:-*/java}\"
      cmd_args: \"${APP_CMD_ARGS}\"" ;;
            exe)
                entry="${entry}
      exe_path: \"${APP_MATCH_VALUE}\"" ;;
            lang)
                entry="${entry}
      languages: \"${APP_MATCH_VALUE}\"" ;;
            container)
                entry="${entry}
      container_name: \"${APP_MATCH_VALUE}\"" ;;
            pid)
                local pid_array
                pid_array=$(echo "${APP_MATCH_VALUE}" | tr ',' '\n' | sed 's/^[[:space:]]*//' | awk '{printf "      - %s\n", $0}')
                entry="${entry}
      target_pids:
${pid_array}" ;;
            *)
                die "未知的 APP_MATCH_TYPE: ${APP_MATCH_TYPE} (支援: port|cmd|exe|lang|container|pid)" ;;
        esac
        APP_ENTRIES+=("$entry")
        ok "已添加應用: ${APP_NAME:-my-app} (${APP_MATCH_TYPE})"
        return
    fi

    # 互動模式:迴圈添加應用
    while true; do
        echo ""
        collect_one_app
        echo ""
        if ! prompt_yesno "是否繼續添加另一個應用?"; then
            break
        fi
    done

    # 可選:排除規則
    echo ""
    if prompt_yesno "是否配置排除規則 (exclude_instrument)?"; then
        echo -e "    輸入要排除的可執行檔路徑 Glob"
        echo -e "    樣本: {obi,prometheus,otelcol*,node_exporter}"
        local exclude_val
        prompt_input exclude_val "排除路徑 Glob"
        EXCLUDE_PATTERN="$exclude_val"
        ok "已添加排除規則: ${exclude_val}"
    fi
}

# ========================= 組建組態檔案 =========================
generate_config() {
    local endpoint_metrics="${OTLP_ENDPOINT}/v1/metrics"

    # 拼接 instrument 段
    local instrument_yaml=""
    for entry in "${APP_ENTRIES[@]}"; do
        instrument_yaml="${instrument_yaml}
${entry}"
    done

    # 拼接 exclude 段
    local exclude_yaml=""
    if [[ -n "$EXCLUDE_PATTERN" ]]; then
        exclude_yaml="
  exclude_instrument:
    - exe_path: \"${EXCLUDE_PATTERN}\""
    fi

    cat > "${CONFIG_FILE}" <<YAML
# OBI 設定檔 - 由 obi-setup.sh 自動產生
# 產生時間: $(date '+%Y-%m-%d %H:%M:%S')

log_level: WARN

# ========== Metrics 匯出 ==========
otel_metrics_export:
  endpoint: ${endpoint_metrics}

metrics:
  features:
    - application
    - network
    - stats_tcp_rtt

routes:
  unmatched: heuristic

# ========== eBPF 探針配置 ==========
ebpf:
  context_propagation: all
  track_request_headers: true
  payload_extraction:
    http:
      genai:
        openai:
          enabled: true
        anthropic:
          enabled: true
        jsonrpc:
          enabled: true
        mcp:
          enabled: true
        qwen:
          enabled: true
        rerank:
          enabled: true
        embedding:
          enabled: true
  buffer_sizes:
    http: 8192

# ========== Traces 匯出 ==========
otel_traces_export:
  endpoint: ${OTLP_ENDPOINT}
  instrumentations:
    - http
    - grpc
    - sql
    - redis
    - kafka
    - mqtt
    - mongo
    - couchbase
    - memcached
    - genai

javaagent:
  enabled: false

# ========== 應用發現規則 ==========
discovery:
  instrument:${instrument_yaml}${exclude_yaml}
YAML

    ok "設定檔已產生: ${CONFIG_FILE}"
}

# ========================= 下載 OBI =========================
download_obi() {
    local arch
    arch=$(detect_arch)
    local url="${OSS_BASE}/obi-linux-${arch}"

    info "下載 OBI v${VERSION} (${arch})..."
    mkdir -p "${INSTALL_DIR}"

    if command -v wget &>/dev/null; then
        wget -q --show-progress "${url}" -O "${OBI_BINARY}"
    elif command -v curl &>/dev/null; then
        curl -fL --progress-bar "${url}" -o "${OBI_BINARY}"
    else
        die "wget 和 curl 均不可用"
    fi

    chmod +x "${OBI_BINARY}"
    ok "OBI 已下載至 ${OBI_BINARY}"
}

# ========================= Systemd 服務 =========================
install_systemd() {
    info "建立 systemd 服務..."

    cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=OBI eBPF Instrumentation Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=${INSTALL_DIR}
Environment="OTEL_EBPF_CONFIG_PATH=${CONFIG_FILE}"
Environment="OTEL_EXPORTER_OTLP_HEADERS=${OTLP_HEADERS}"
ExecStart=${OBI_BINARY}
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_NAME}

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
    systemctl enable "${SERVICE_NAME}" --quiet
    ok "Systemd 服務已建立並設為開機自啟"
}

start_service() {
    info "啟動 OBI 服務..."
    systemctl start "${SERVICE_NAME}"
    sleep 2

    if systemctl is-active --quiet "${SERVICE_NAME}"; then
        ok "OBI 服務啟動成功!"
    else
        fail "OBI 服務啟動失敗, 錯誤記錄檔如下:"
        journalctl -u "${SERVICE_NAME}" -n 30 --no-pager
        exit 1
    fi
}

# ========================= Docker 部署 =========================
deploy_docker() {
    if ! command -v docker &>/dev/null; then
        die "Docker 未安裝"
    fi

    collect_config
    mkdir -p "${INSTALL_DIR}"
    generate_config

    info "拉取 Docker 鏡像: ${DOCKER_IMAGE}"
    docker pull "${DOCKER_IMAGE}"

    if docker ps -a --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        warn "移除已有容器..."
        docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1
    fi

    info "啟動 OBI 容器..."
    docker run -d \
        --name "${SERVICE_NAME}" \
        --pid=host \
        --privileged \
        --restart=always \
        -v "${CONFIG_FILE}:/config/config.yaml:ro" \
        -e "OTEL_EXPORTER_OTLP_HEADERS=${OTLP_HEADERS}" \
        -e "OTEL_EXPORTER_OTLP_ENDPOINT=${OTLP_ENDPOINT}" \
        "${DOCKER_IMAGE}" \
        --config=/config/config.yaml

    sleep 2
    if docker ps --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        ok "OBI 容器啟動成功!"
    else
        fail "容器啟動失敗:"
        docker logs "${SERVICE_NAME}" --tail 30
        exit 1
    fi

    echo ""
    echo -e "${BOLD}Docker 管理命令:${NC}"
    echo "  查看日誌:   docker logs -f ${SERVICE_NAME}"
    echo "  停止容器:   docker stop ${SERVICE_NAME}"
    echo "  刪除容器:   docker rm -f ${SERVICE_NAME}"
    echo ""
}

# ========================= 卸載 =========================
uninstall() {
    echo -e "${BOLD}========== 卸載 OBI ==========${NC}"

    if systemctl is-active --quiet "${SERVICE_NAME}" 2>/dev/null; then
        systemctl stop "${SERVICE_NAME}"
    fi
    if [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
        systemctl disable "${SERVICE_NAME}" --quiet 2>/dev/null || true
        rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
        systemctl daemon-reload
        ok "Systemd 服務已移除"
    fi

    if command -v docker &>/dev/null && docker ps -a --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1
        ok "Docker 容器已移除"
    fi

    if [[ -d "${INSTALL_DIR}" ]]; then
        rm -rf "${INSTALL_DIR}"
        ok "安裝目錄已清理"
    fi

    ok "OBI 已完全卸載"
}

# ========================= 狀態查看 =========================
show_status() {
    echo -e "${BOLD}========== OBI 運行狀態 ==========${NC}"

    if [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
        echo -e "\n${CYAN}[Systemd 服務]${NC}"
        systemctl status "${SERVICE_NAME}" --no-pager 2>/dev/null || true
        echo -e "\n${CYAN}[最近日誌]${NC}"
        journalctl -u "${SERVICE_NAME}" -n 20 --no-pager 2>/dev/null || true
    fi

    if command -v docker &>/dev/null && docker ps -a --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        echo -e "\n${CYAN}[Docker 容器]${NC}"
        docker ps -f "name=${SERVICE_NAME}"
        echo -e "\n${CYAN}[最近日誌]${NC}"
        docker logs "${SERVICE_NAME}" --tail 20 2>&1 || true
    fi

    if [[ -f "${CONFIG_FILE}" ]]; then
        echo -e "\n${CYAN}[設定檔: ${CONFIG_FILE}]${NC}"
        cat "${CONFIG_FILE}"
    fi
}

# ========================= 二進位安裝主流程 =========================
install_binary() {
    check_env || die "環境檢查未通過"
    collect_config
    download_obi
    generate_config
    install_systemd
    start_service

    echo ""
    echo -e "${GREEN}============================================${NC}"
    echo -e "${GREEN}  OBI 部署完成!${NC}"
    echo -e "${GREEN}============================================${NC}"
    echo ""
    echo -e "${BOLD}安裝目錄:${NC}   ${INSTALL_DIR}"
    echo -e "${BOLD}設定檔:${NC}   ${CONFIG_FILE}"
    echo ""
    echo -e "常用命令:"
    echo "  查看狀態:   sudo systemctl status ${SERVICE_NAME}"
    echo "  查看日誌:   sudo journalctl -u ${SERVICE_NAME} -f"
    echo "  重啟服務:   sudo systemctl restart ${SERVICE_NAME}"
    echo "  卸載:       sudo bash $0 uninstall"
    echo ""
}

# ========================= 入口 =========================
print_usage() {
    echo ""
    echo -e "${BOLD}OBI 一鍵部署指令碼 v${VERSION}${NC}"
    echo ""
    echo "用法: sudo bash $0 <命令>"
    echo ""
    echo "命令:"
    echo "  install    二進位方式安裝 (systemd 管理)"
    echo "  docker     Docker 方式部署"
    echo "  check      僅執行環境檢查"
    echo "  status     查看運行狀態與日誌"
    echo "  uninstall  卸載 OBI"
    echo ""
    echo "應用匹配方式 (APP_MATCH_TYPE):"
    echo "  port       連接埠號碼匹配 (open_ports)"
    echo "  cmd        命令列匹配 (exe_path + cmd_args)"
    echo "  exe        可執行檔路徑匹配 (exe_path)"
    echo "  lang       程式設計語言自動探索 (languages)"
    echo "  container  Docker 容器名匹配 (container_name)"
    echo "  pid        PID 指定 (target_pids)"
    echo ""
}

main() {
    local action="${1:-}"
    case "$action" in
        install)   check_root "$@"; install_binary ;;
        docker)    check_root "$@"; check_env || die "環境檢查未通過"; deploy_docker ;;
        check)     check_root "$@"; check_env ;;
        uninstall) check_root "$@"; uninstall ;;
        status)    check_root "$@"; show_status ;;
        *)         print_usage; exit 1 ;;
    esac
}

main "$@"

1. 擷取存取點資訊

登入 ARMS 控制台,在左側導覽列單擊「接入中心」,在服務端應用地區單擊 OpenTelemetry 卡片,擷取以下兩個值:

  • OTEL_EXPORTER_OTLP_ENDPOINT(如 http://xxx:4318\

  • OTEL_EXPORTER_OTLP_HEADERS(如 Authentication=xxx

2. 下載並執行指令碼

chmod +x obi-setup.sh

3. 互動式安裝(二進位 + systemd)

sudo bash obi-setup.sh install

指令碼會依次完成:環境檢查 → 收集 endpoint/headers → 選擇應用匹配方式(支援 6 種情境) → 支援添加多個應用 → 可選配置排除規則/採樣/匯出控制 → 自動檢測 CPU 架構並下載 OBI → 產生 config.yaml → 建立 systemd 服務並啟動。

4. Docker 方式部署

sudo bash obi-setup.sh docker

5. 非互動式部署(批量情境)

通過環境變數傳參跳過互動。支援以下匹配類型:

# 按連接埠匹配
export OTLP_ENDPOINT="http://xxx:4318"
export OTLP_HEADERS="Authentication=xxx"
export APP_NAME="my-app"
export APP_MATCH_TYPE="port"
export APP_MATCH_VALUE="8080"
sudo -E bash obi-setup.sh install

# Java 應用 (exe_path + cmd_args)
export APP_MATCH_TYPE="cmd"
export APP_EXE_PATH="*/java"
export APP_CMD_ARGS="*petclinic*"

# 按程式設計語言
export APP_MATCH_TYPE="lang"
export APP_MATCH_VALUE="java"

# 按可執行檔路徑
export APP_MATCH_TYPE="exe"
export APP_MATCH_VALUE="*/my-server"

# 按 Docker 容器名
export APP_MATCH_TYPE="container"
export APP_MATCH_VALUE="my-container-*"

# 按 PID
export APP_MATCH_TYPE="pid"
export APP_MATCH_VALUE="12345,67890"

6. 其他命令

sudo bash obi-setup.sh check      # 僅執行環境檢查
sudo bash obi-setup.sh status     # 查看運行狀態與日誌
sudo bash obi-setup.sh uninstall  # 卸載 OBI

方式二:手動分步部署

如果你需要更精細地控制每一步,可以按以下步驟手動操作。

步驟一:擷取存取點資訊

同上,從 ARMS 控制台 擷取 OTEL_EXPORTER_OTLP_ENDPOINTOTEL_EXPORTER_OTLP_HEADERS

步驟二:下載 OBI

# amd64 架構
wget https://arms-apm-cn-hangzhou-pre.oss-cn-hangzhou.aliyuncs.com/obi-agent/0.9.5/obi-linux-amd64 -O obi

# arm64 架構
wget https://arms-apm-cn-hangzhou-pre.oss-cn-hangzhou.aliyuncs.com/obi-agent/0.9.5/obi-linux-arm64 -O obi

chmod +x obi

步驟三:編寫設定檔

建立 config.yaml,將 <OTLP_ENDPOINT> 替換為實際值,並根據應用情況修改 discovery.instrument 部分(參見上方「應用發現匹配方式」章節選擇合適的配置):

log_level: WARN

otel_metrics_export:
  endpoint: <OTLP_ENDPOINT>/v1/metrics

metrics:
  features:
    - application
    - network
    - stats_tcp_rtt

routes:
  unmatched: heuristic

ebpf:
  context_propagation: all
  track_request_headers: true
  payload_extraction:
    http:
      genai:
        openai:
          enabled: true
        anthropic:
          enabled: true
        jsonrpc:
          enabled: true
        mcp:
          enabled: true
        qwen:
          enabled: true
        rerank:
          enabled: true
        embedding:
          enabled: true
  buffer_sizes:
    http: 8192

otel_traces_export:
  endpoint: <OTLP_ENDPOINT>
  instrumentations:
    - http
    - grpc
    - sql
    - redis
    - kafka
    - mqtt
    - mongo
    - couchbase
    - memcached
    - genai

javaagent:
  enabled: false

discovery:
  instrument:
    # 根據實際情境選擇匹配方式,參見上方「應用發現匹配方式」
    - name: my-app
      open_ports: 8080

步驟四:部署服務

二進位方式(systemd 管理):

建立 systemd 服務檔案 /etc/systemd/system/obi.service

[Unit]
Description=OBI eBPF Instrumentation Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/path/to/obi-dir
Environment="OTEL_EBPF_CONFIG_PATH=config.yaml"
Environment="OTEL_EXPORTER_OTLP_HEADERS=<替換為實際值>"
ExecStart=/path/to/obi-dir/obi
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=obi

[Install]
WantedBy=multi-user.target

啟動服務:

sudo systemctl daemon-reload
sudo systemctl enable obi
sudo systemctl start obi
sudo systemctl status obi

Docker 方式:

docker pull registry.cn-hangzhou.aliyuncs.com/private-mesh/obi:0.9.5

docker run -d \
  --name obi \
  --pid=host \
  --privileged \
  --restart=always \
  -v $(pwd)/config.yaml:/config/config.yaml:ro \
  -e OTEL_EXPORTER_OTLP_HEADERS="<替換為實際值>" \
  -e OTEL_EXPORTER_OTLP_ENDPOINT="<替換為實際值>" \
  registry.cn-hangzhou.aliyuncs.com/private-mesh/obi:0.9.5 \
  --config=/config/config.yaml

查看監控資料

登入 ARMS 控制台,選擇目標工作空間,在左側導覽列選擇「所有功能 → 應用監控」,在應用列表中找到你配置的應用程式名稱,點擊即可查看拓撲、鏈路、異常等監控詳情。

常用營運命令速查

操作

systemd 方式

Docker 方式

查看狀態

sudo systemctl status obi

docker ps -f name=obi

查看日誌

sudo journalctl -u obi -f

docker logs -f obi

重啟

sudo systemctl restart obi

docker restart obi

停止

sudo systemctl stop obi

docker stop obi

卸載

sudo bash obi-setup.sh uninstall

docker rm -f obi

附錄:一鍵部署指令碼(obi-setup.sh)

將以下內容儲存為 obi-setup.sh,賦予執行許可權後使用。

#!/bin/bash
# ============================================================================
#  OBI (OpenTelemetry eBPF Instrumentation) 一鍵部署指令碼
#  合并了環境檢查、下載、配置產生、服務安裝的完整流程
#
#  用法:
#    sudo bash obi-setup.sh install   — 互動式安裝(二進位 + systemd)
#    sudo bash obi-setup.sh docker    — Docker 方式部署
#    sudo bash obi-setup.sh check     — 僅做環境檢查
#    sudo bash obi-setup.sh uninstall — 卸載 OBI 服務
#    sudo bash obi-setup.sh status    — 查看運行狀態與日誌
#
#  非互動式部署(環境變數):
#    export OTLP_ENDPOINT="http://xxx:4318"
#    export OTLP_HEADERS="Authentication=xxx"
#    export APP_NAME="my-app"
#    export APP_MATCH_TYPE="port"    # port|cmd|exe|lang|container|pid
#    export APP_MATCH_VALUE="8080"
#    sudo -E bash obi-setup.sh install
# ============================================================================

set -euo pipefail

# ========================= 全域變數 =========================
VERSION="0.9.5"
SERVICE_NAME="obi"
INSTALL_DIR="/opt/obi"
CONFIG_FILE="${INSTALL_DIR}/config.yaml"
OBI_BINARY="${INSTALL_DIR}/obi"
DOCKER_IMAGE="registry.cn-hangzhou.aliyuncs.com/private-mesh/obi:${VERSION}"
OSS_BASE="https://arms-apm-cn-hangzhou-pre.oss-cn-hangzhou.aliyuncs.com/obi-agent/${VERSION}"

# 顏色
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

# 應用列表(數組)
declare -a APP_ENTRIES=()
# 排除規則
EXCLUDE_PATTERN=""

# ========================= 工具函數 =========================
info()  { echo -e "${CYAN}[INFO]${NC} $*"; }
ok()    { echo -e "${GREEN}[OK]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
fail()  { echo -e "${RED}[FAIL]${NC} $*"; }
die()   { fail "$*"; exit 1; }

prompt_input() {
    local var_name="$1" prompt_msg="$2" default_val="${3:-}"
    local current_val="${!var_name:-}"
    if [[ -n "$current_val" ]]; then
        info "使用環境變數 ${var_name}=${current_val}"
        return
    fi
    if [[ -n "$default_val" ]]; then
        read -rp "$(echo -e "${BOLD}${prompt_msg} [${default_val}]: ${NC}")" input
        eval "$var_name=\"${input:-$default_val}\""
    else
        while true; do
            read -rp "$(echo -e "${BOLD}${prompt_msg}: ${NC}")" input
            if [[ -n "$input" ]]; then
                eval "$var_name=\"$input\""
                return
            fi
            warn "此項為必填"
        done
    fi
}

prompt_yesno() {
    local prompt_msg="$1" default="${2:-n}"
    local yn
    read -rp "$(echo -e "${BOLD}${prompt_msg} [y/N]: ${NC}")" yn
    case "${yn:-$default}" in
        [Yy]*) return 0 ;;
        *) return 1 ;;
    esac
}

# ========================= 環境檢查 =========================
check_root() {
    if [[ "$EUID" -ne 0 ]]; then
        die "請使用 root 許可權運行: sudo bash $0 $*"
    fi
    ok "Root 許可權"
}

check_env() {
    echo ""
    echo -e "${BOLD}========== 環境檢查 ==========${NC}"

    local kernel_ver major minor
    kernel_ver=$(uname -r)
    major=$(echo "$kernel_ver" | cut -d. -f1)
    minor=$(echo "$kernel_ver" | cut -d. -f2)

    local is_rhel=false
    if [[ -f /etc/redhat-release ]] || grep -qi 'rhel\|centos\|rocky\|alma\|anolis\|alinux' /etc/os-release 2>/dev/null; then
        is_rhel=true
    fi

    if [[ "$major" -gt 5 ]] || { [[ "$major" -eq 5 ]] && [[ "$minor" -ge 8 ]]; }; then
        ok "核心版本: ${kernel_ver} (>= 5.8)"
    elif $is_rhel && { [[ "$major" -gt 4 ]] || { [[ "$major" -eq 4 ]] && [[ "$minor" -ge 18 ]]; }; }; then
        ok "核心版本: ${kernel_ver} (RHEL 系, >= 4.18)"
    else
        fail "核心版本: ${kernel_ver} (最低要求 5.8, RHEL 系 4.18)"
        return 1
    fi

    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) ok "CPU 架構: ${arch} (amd64)" ;;
        aarch64|arm64) ok "CPU 架構: ${arch} (arm64)" ;;
        *) fail "CPU 架構: ${arch} (不支援)"; return 1 ;;
    esac

    if [[ -f /sys/kernel/btf/vmlinux ]]; then
        ok "BTF: 可用"
    else
        fail "BTF: /sys/kernel/btf/vmlinux 不存在"
        return 1
    fi

    echo -e "${GREEN}========== 環境檢查通過 ==========${NC}"
    echo ""
    return 0
}

detect_arch() {
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) echo "amd64" ;;
        aarch64|arm64) echo "arm64" ;;
        *) die "不支援的 CPU 架構: ${arch}" ;;
    esac
}

# ========================= 單個應用配置收集 =========================
# 產生單個 instrument entry 的 YAML 文本(每行縮排 4 空格 + 屬性縮排 6 空格)
collect_one_app() {
    local app_name match_type
    local entry=""

    prompt_input app_name "應用程式名稱 (ARMS 控制台顯示名稱)" "my-app"

    echo ""
    echo -e "  ${BOLD}選擇應用匹配方式:${NC}"
    echo -e "    1) 連接埠號碼匹配        — open_ports, 如 8080 或 80,443 或 8000-8999"
    echo -e "    2) 命令列匹配        — exe_path + cmd_args, 適合 java -jar 等情境"
    echo -e "    3) 可執行檔路徑匹配 — exe_path, 適合自編譯二進位"
    echo -e "    4) 程式設計語言自動探索   — languages, 自動檢測 go/java/python/nodejs 等"
    echo -e "    5) Docker 容器名匹配  — container_name, 適合 ECS 上跑 Docker"
    echo -e "    6) PID 指定           — target_pids, 臨時調試用"
    read -rp "$(echo -e "${BOLD}選擇 [1-6, 預設1]: ${NC}")" match_choice

    entry="    - name: ${app_name}"

    case "${match_choice:-1}" in
        1)
            local ports
            prompt_input ports "監聽連接埠號碼 (如 8080, 80,443, 8000-8999)" "8080"
            entry="${entry}
      open_ports: ${ports}"
            ;;
        2)
            local exe_path cmd_args
            prompt_input exe_path "可執行檔路徑 Glob (如 */java)" "*/java"
            prompt_input cmd_args "命令列參數 Glob (如 *petclinic*)"
            entry="${entry}
      exe_path: \"${exe_path}\"
      cmd_args: \"${cmd_args}\""
            ;;
        3)
            local exe_path
            prompt_input exe_path "可執行檔路徑 Glob (如 */my-server, */nginx)"
            entry="${entry}
      exe_path: \"${exe_path}\""
            ;;
        4)
            local lang
            echo -e "    支援的語言: go, java, python, ruby, nodejs, rust, dotnet, php, cpp"
            echo -e "    多語言用花括弧: {go,rust} 或 {python,nodejs}"
            prompt_input lang "程式設計語言" "java"
            entry="${entry}
      languages: \"${lang}\""
            ;;
        5)
            local cname
            prompt_input cname "Docker 容器名 Glob (如 my-container-*)"
            entry="${entry}
      container_name: \"${cname}\""
            ;;
        6)
            local pids
            prompt_input pids "PID 列表, 逗號分隔 (如 12345,67890)"
            # 轉成 YAML 數組
            local pid_array
            pid_array=$(echo "$pids" | tr ',' '\n' | sed 's/^[[:space:]]*//' | awk '{printf "      - %s\n", $0}')
            entry="${entry}
      target_pids:
${pid_array}"
            ;;
        *)
            local ports
            prompt_input ports "監聽連接埠號碼" "8080"
            entry="${entry}
      open_ports: ${ports}"
            ;;
    esac

    # 可選:僅容器進程
    if [[ "${match_choice:-1}" != "5" ]]; then
        if prompt_yesno "是否僅匹配容器內進程 (containers_only)?"; then
            entry="${entry}
      containers_only: true"
        fi
    fi

    # 可選:採樣
    if prompt_yesno "是否配置 Traces 採樣 (減少資料量)?"; then
        echo -e "    採樣器: always_on, always_off, traceidratio, parentbased_traceidratio"
        local sampler_name sampler_arg
        prompt_input sampler_name "採樣器名稱" "traceidratio"
        prompt_input sampler_arg  "採樣參數 (如 0.5 表示採樣50%)" "1.0"
        entry="${entry}
      sampler:
        name: ${sampler_name}
        arg: \"${sampler_arg}\""
    fi

    APP_ENTRIES+=("$entry")
    ok "已添加應用: ${app_name}"
}

# ========================= 收集配置(完整) =========================
collect_config() {
    echo -e "${BOLD}========== 配置資訊 ==========${NC}"
    echo ""
    info "請從 ARMS 接入中心擷取以下資訊:"
    info "  ARMS 控制台 -> 接入中心 -> OpenTelemetry 卡片 -> 點擊擷取 LicenseKey"
    echo ""

    prompt_input OTLP_ENDPOINT "OTEL_EXPORTER_OTLP_ENDPOINT (如 http://xxx:4318)"
    prompt_input OTLP_HEADERS  "OTEL_EXPORTER_OTLP_HEADERS  (如 Authentication=xxx)"

    echo ""
    echo -e "${BOLD}========== 應用配置 ==========${NC}"

    # 非互動模式:從環境變數構建單個 app
    if [[ -n "${APP_MATCH_TYPE:-}" ]]; then
        local entry="    - name: ${APP_NAME:-my-app}"
        case "$APP_MATCH_TYPE" in
            port)
                entry="${entry}
      open_ports: ${APP_MATCH_VALUE:-8080}" ;;
            cmd)
                entry="${entry}
      exe_path: \"${APP_EXE_PATH:-*/java}\"
      cmd_args: \"${APP_CMD_ARGS}\"" ;;
            exe)
                entry="${entry}
      exe_path: \"${APP_MATCH_VALUE}\"" ;;
            lang)
                entry="${entry}
      languages: \"${APP_MATCH_VALUE}\"" ;;
            container)
                entry="${entry}
      container_name: \"${APP_MATCH_VALUE}\"" ;;
            pid)
                local pid_array
                pid_array=$(echo "${APP_MATCH_VALUE}" | tr ',' '\n' | sed 's/^[[:space:]]*//' | awk '{printf "      - %s\n", $0}')
                entry="${entry}
      target_pids:
${pid_array}" ;;
            *)
                die "未知的 APP_MATCH_TYPE: ${APP_MATCH_TYPE} (支援: port|cmd|exe|lang|container|pid)" ;;
        esac
        APP_ENTRIES+=("$entry")
        ok "已添加應用: ${APP_NAME:-my-app} (${APP_MATCH_TYPE})"
        return
    fi

    # 互動模式:迴圈添加應用
    while true; do
        echo ""
        collect_one_app
        echo ""
        if ! prompt_yesno "是否繼續添加另一個應用?"; then
            break
        fi
    done

    # 可選:排除規則
    echo ""
    if prompt_yesno "是否配置排除規則 (exclude_instrument)?"; then
        echo -e "    輸入要排除的可執行檔路徑 Glob"
        echo -e "    樣本: {obi,prometheus,otelcol*,node_exporter}"
        local exclude_val
        prompt_input exclude_val "排除路徑 Glob"
        EXCLUDE_PATTERN="$exclude_val"
        ok "已添加排除規則: ${exclude_val}"
    fi
}

# ========================= 組建組態檔案 =========================
generate_config() {
    local endpoint_metrics="${OTLP_ENDPOINT}/v1/metrics"

    # 拼接 instrument 段
    local instrument_yaml=""
    for entry in "${APP_ENTRIES[@]}"; do
        instrument_yaml="${instrument_yaml}
${entry}"
    done

    # 拼接 exclude 段
    local exclude_yaml=""
    if [[ -n "$EXCLUDE_PATTERN" ]]; then
        exclude_yaml="
  exclude_instrument:
    - exe_path: \"${EXCLUDE_PATTERN}\""
    fi

    cat > "${CONFIG_FILE}" <<YAML
# OBI 設定檔 - 由 obi-setup.sh 自動產生
# 產生時間: $(date '+%Y-%m-%d %H:%M:%S')

log_level: WARN

# ========== Metrics 匯出 ==========
otel_metrics_export:
  endpoint: ${endpoint_metrics}

metrics:
  features:
    - application
    - network
    - stats_tcp_rtt

routes:
  unmatched: heuristic

# ========== eBPF 探針配置 ==========
ebpf:
  context_propagation: all
  track_request_headers: true
  payload_extraction:
    http:
      genai:
        openai:
          enabled: true
        anthropic:
          enabled: true
        jsonrpc:
          enabled: true
        mcp:
          enabled: true
        qwen:
          enabled: true
        rerank:
          enabled: true
        embedding:
          enabled: true
  buffer_sizes:
    http: 8192

# ========== Traces 匯出 ==========
otel_traces_export:
  endpoint: ${OTLP_ENDPOINT}
  instrumentations:
    - http
    - grpc
    - sql
    - redis
    - kafka
    - mqtt
    - mongo
    - couchbase
    - memcached
    - genai

javaagent:
  enabled: false

# ========== 應用發現規則 ==========
discovery:
  instrument:${instrument_yaml}${exclude_yaml}
YAML

    ok "設定檔已產生: ${CONFIG_FILE}"
}

# ========================= 下載 OBI =========================
download_obi() {
    local arch
    arch=$(detect_arch)
    local url="${OSS_BASE}/obi-linux-${arch}"

    info "下載 OBI v${VERSION} (${arch})..."
    mkdir -p "${INSTALL_DIR}"

    if command -v wget &>/dev/null; then
        wget -q --show-progress "${url}" -O "${OBI_BINARY}"
    elif command -v curl &>/dev/null; then
        curl -fL --progress-bar "${url}" -o "${OBI_BINARY}"
    else
        die "wget 和 curl 均不可用"
    fi

    chmod +x "${OBI_BINARY}"
    ok "OBI 已下載至 ${OBI_BINARY}"
}

# ========================= Systemd 服務 =========================
install_systemd() {
    info "建立 systemd 服務..."

    cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=OBI eBPF Instrumentation Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=${INSTALL_DIR}
Environment="OTEL_EBPF_CONFIG_PATH=${CONFIG_FILE}"
Environment="OTEL_EXPORTER_OTLP_HEADERS=${OTLP_HEADERS}"
ExecStart=${OBI_BINARY}
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_NAME}

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
    systemctl enable "${SERVICE_NAME}" --quiet
    ok "Systemd 服務已建立並設為開機自啟"
}

start_service() {
    info "啟動 OBI 服務..."
    systemctl start "${SERVICE_NAME}"
    sleep 2

    if systemctl is-active --quiet "${SERVICE_NAME}"; then
        ok "OBI 服務啟動成功!"
    else
        fail "OBI 服務啟動失敗, 錯誤記錄檔如下:"
        journalctl -u "${SERVICE_NAME}" -n 30 --no-pager
        exit 1
    fi
}

# ========================= Docker 部署 =========================
deploy_docker() {
    if ! command -v docker &>/dev/null; then
        die "Docker 未安裝"
    fi

    collect_config
    mkdir -p "${INSTALL_DIR}"
    generate_config

    info "拉取 Docker 鏡像: ${DOCKER_IMAGE}"
    docker pull "${DOCKER_IMAGE}"

    if docker ps -a --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        warn "移除已有容器..."
        docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1
    fi

    info "啟動 OBI 容器..."
    docker run -d \
        --name "${SERVICE_NAME}" \
        --pid=host \
        --privileged \
        --restart=always \
        -v "${CONFIG_FILE}:/config/config.yaml:ro" \
        -e "OTEL_EXPORTER_OTLP_HEADERS=${OTLP_HEADERS}" \
        -e "OTEL_EXPORTER_OTLP_ENDPOINT=${OTLP_ENDPOINT}" \
        "${DOCKER_IMAGE}" \
        --config=/config/config.yaml

    sleep 2
    if docker ps --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        ok "OBI 容器啟動成功!"
    else
        fail "容器啟動失敗:"
        docker logs "${SERVICE_NAME}" --tail 30
        exit 1
    fi

    echo ""
    echo -e "${BOLD}Docker 管理命令:${NC}"
    echo "  查看日誌:   docker logs -f ${SERVICE_NAME}"
    echo "  停止容器:   docker stop ${SERVICE_NAME}"
    echo "  刪除容器:   docker rm -f ${SERVICE_NAME}"
    echo ""
}

# ========================= 卸載 =========================
uninstall() {
    echo -e "${BOLD}========== 卸載 OBI ==========${NC}"

    if systemctl is-active --quiet "${SERVICE_NAME}" 2>/dev/null; then
        systemctl stop "${SERVICE_NAME}"
    fi
    if [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
        systemctl disable "${SERVICE_NAME}" --quiet 2>/dev/null || true
        rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
        systemctl daemon-reload
        ok "Systemd 服務已移除"
    fi

    if command -v docker &>/dev/null && docker ps -a --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1
        ok "Docker 容器已移除"
    fi

    if [[ -d "${INSTALL_DIR}" ]]; then
        rm -rf "${INSTALL_DIR}"
        ok "安裝目錄已清理"
    fi

    ok "OBI 已完全卸載"
}

# ========================= 狀態查看 =========================
show_status() {
    echo -e "${BOLD}========== OBI 運行狀態 ==========${NC}"

    if [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
        echo -e "\n${CYAN}[Systemd 服務]${NC}"
        systemctl status "${SERVICE_NAME}" --no-pager 2>/dev/null || true
        echo -e "\n${CYAN}[最近日誌]${NC}"
        journalctl -u "${SERVICE_NAME}" -n 20 --no-pager 2>/dev/null || true
    fi

    if command -v docker &>/dev/null && docker ps -a --format '{{.Names}}' | grep -q "^${SERVICE_NAME}$"; then
        echo -e "\n${CYAN}[Docker 容器]${NC}"
        docker ps -f "name=${SERVICE_NAME}"
        echo -e "\n${CYAN}[最近日誌]${NC}"
        docker logs "${SERVICE_NAME}" --tail 20 2>&1 || true
    fi

    if [[ -f "${CONFIG_FILE}" ]]; then
        echo -e "\n${CYAN}[設定檔: ${CONFIG_FILE}]${NC}"
        cat "${CONFIG_FILE}"
    fi
}

# ========================= 二進位安裝主流程 =========================
install_binary() {
    check_env || die "環境檢查未通過"
    collect_config
    download_obi
    generate_config
    install_systemd
    start_service

    echo ""
    echo -e "${GREEN}============================================${NC}"
    echo -e "${GREEN}  OBI 部署完成!${NC}"
    echo -e "${GREEN}============================================${NC}"
    echo ""
    echo -e "${BOLD}安裝目錄:${NC}   ${INSTALL_DIR}"
    echo -e "${BOLD}設定檔:${NC}   ${CONFIG_FILE}"
    echo ""
    echo -e "常用命令:"
    echo "  查看狀態:   sudo systemctl status ${SERVICE_NAME}"
    echo "  查看日誌:   sudo journalctl -u ${SERVICE_NAME} -f"
    echo "  重啟服務:   sudo systemctl restart ${SERVICE_NAME}"
    echo "  卸載:       sudo bash $0 uninstall"
    echo ""
}

# ========================= 入口 =========================
print_usage() {
    echo ""
    echo -e "${BOLD}OBI 一鍵部署指令碼 v${VERSION}${NC}"
    echo ""
    echo "用法: sudo bash $0 <命令>"
    echo ""
    echo "命令:"
    echo "  install    二進位方式安裝 (systemd 管理)"
    echo "  docker     Docker 方式部署"
    echo "  check      僅執行環境檢查"
    echo "  status     查看運行狀態與日誌"
    echo "  uninstall  卸載 OBI"
    echo ""
    echo "應用匹配方式 (APP_MATCH_TYPE):"
    echo "  port       連接埠號碼匹配 (open_ports)"
    echo "  cmd        命令列匹配 (exe_path + cmd_args)"
    echo "  exe        可執行檔路徑匹配 (exe_path)"
    echo "  lang       程式設計語言自動探索 (languages)"
    echo "  container  Docker 容器名匹配 (container_name)"
    echo "  pid        PID 指定 (target_pids)"
    echo ""
}

main() {
    local action="${1:-}"
    case "$action" in
        install)   check_root "$@"; install_binary ;;
        docker)    check_root "$@"; check_env || die "環境檢查未通過"; deploy_docker ;;
        check)     check_root "$@"; check_env ;;
        uninstall) check_root "$@"; uninstall ;;
        status)    check_root "$@"; show_status ;;
        *)         print_usage; exit 1 ;;
    esac
}

main "$@"