全部产品
Search
文档中心

应用实时监控服务ARMS:ECS上通过OpenTelemetry eBPF(OBI)接入可观测

更新时间:Jul 01, 2026

通过OpenTelemetry eBPF Instrumentation(OBI)提供的无侵入监控能力上报链路数据至ARMS后,ARMS即可开始监控应用,您可以查看应用拓扑、调用链路、异常事务、慢事务和SQL分析等一系列监控数据。本文介绍如何使用OBI为应用进行监控并上报数据。

前提条件

说明

如果您在使用OpenTelemetry eBPF过程中有任何问题,欢迎通过钉钉答疑群(群号:159215000379)与我们联系。

  • 已准备一台满足以下条件的 Linux 服务器(云服务器 ECS 或本地环境均可)。

    要求

    支持范围

    内核版本

    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 "$@"