告别代码修改:传统 VM 环境下的 OpenTelemetry 自动注入

告别代码修改:传统 VM 环境下的 OpenTelemetry 自动注入

在微服务架构中,可观测性就像是给应用装上 " 眼睛 " 和 " 耳朵 “。传统方式需要在每个服务中手动集成监控代码,而 OpenTelemetry Injector 提供了一种更优雅的解决方案。

TL;DR

OpenTelemetry Injector 是一个专为传统 VM 环境设计的零代码观测工具。通过 Linux 的 LD_PRELOAD 机制,无需修改应用代码即可为 Java、Node.js、.NET 应用自动注入 OpenTelemetry 观测能力。核心优势:系统级自动化、多语言统一管理、生产环境就绪。不适合容器/K8s 环境,云原生场景建议使用 OpenTelemetry Operator、init-container、sidecar 等方案。

但是对于企业数据中心中的传统部署、混合技术栈的微服务架构,或者遗留系统的可观测升级,OpenTelemetry Injector 提供了一条优雅而高效的路径。

为什么需要零代码检测?

想象一下,你负责维护一个包含数百个微服务的系统。每个服务都用不同的语言编写(Java、Node.js、.NET),现在需要为它们添加可观测性能力。

传统方式的问题:

  • 需要修改每个服务的代码
  • 不同语言的集成方式各不相同
  • 版本升级时需要重新修改
  • 不同语言中的功能不一致
  • 增加了出错的风险
  • 增加了维护的成本

这就是 OpenTelemetry Injector 诞生的背景。

项目核心价值

OpenTelemetry Injector 是一个开源工具,它允许你在不修改任何应用代码的情况下,为 Java、Node.js 和.NET 应用自动注入 OpenTelemetry 检测代理。

社区生态:除了本项目基于系统级的 LD_PRELOAD 机制,还有其他优秀的零代码检测解决方案。例如,OpenTelemetry Operator 提供了基于 Kubernetes 的自动注入能力。详细对比和实践可以参考我的另一篇文章:在 Kubernetes 中无侵入安装 OpenTelemetry 探针

核心特性

  • 零侵入性:无需修改应用源码
  • 多语言支持:Java、Node.js、.NET 原生支持
  • 开箱即用:提供 Debian/RPM 安装包
  • 生产就绪:经过严格测试的安全实现
  • 灵活配置:支持多种部署和配置方式

实现方式对比

特性 OpenTelemetry Injector OpenTelemetry Operator
部署环境 任意 Linux 系统 Kubernetes 集群
触发机制 系统级 LD_PRELOAD Pod 注解
配置方式 配置文件 + 环境变量 CRD + 注解
适用场景 传统部署、虚拟机 云原生、容器化
管理复杂度 较低 需要 K8s 知识

两种方案都是优秀的零代码检测解决方案,选择取决于你的部署环境和使用场景。

5 分钟快速开始

环境准备

  • Linux 系统(推荐使用 Ubuntu 20.04+)
  • Java 8+
  • Docker(用于编译和测试)

安装 Injector

我们需要从源码编译安装 OpenTelemetry Injector,因为目前还没有发布正式的安装包。

克隆源码并编译:

git clone https://github.com/open-telemetry/opentelemetry-injector.git
cd opentelemetry-injector
make deb-package VERSION=0.0.1

编译完成后可以获得用于安装的 Debian 包 instrumentation/dist/opentelemetry-injector_0.0.1_amd64.deb

安装生成的 Debian 包:

sudo dpkg -i instrumentation/dist/opentelemetry-injector_0.0.1_amd64.deb

安装后,系统中会加入以下内容:

  • 共享库 /usr/lib/opentelemetry/libotelinject.so
  • 对应语言的配置文件 /etc/opentelemetry/otelinject/{java,node,dotnet}.conf
  • 相关的自动检测代理文件目录 /usr/lib/opentelemetry
  • 用于 systemd 服务的示例配置文件 /usr/lib/opentelemetry/examples/systemd/00-opentelemetry-injector.conf

激活 Injector

OpenTelemetry Injector 支持两种激活方式,每种方式都有不同的适用场景和配置方法。

  • 系统级激活(System-wide):需要对所有运行的进程(包括非 systemd 服务)进行自动注入,适用于测试环境或单机部署。
  • 仅 systemd 服务激活(Systemd services only):只需监控 systemd 管理的服务,适用于生产环境;对容器化环境友好,可以通过 systemd 配置管理

这里我们为了演示,选择系统级激活:

sudo sh -c 'echo /usr/lib/opentelemetry/libotelinject.so >> /etc/ld.so.preload'

验证效果

我们以 Java 应用为例,演示如何通过 Injector 注入环境变量并激活 OpenTelemetry Java Agent。

sudo sh -c 'cat >> /etc/opentelemetry/otelinject/java.conf <<EOF
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_LOGS_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0,deployment.environment=development
OTEL_SERVICE_NAME=my-java-service
EOF'

创建一个简单的 Java 应用 Main.java,这里我们只打印环境变量以验证注入是否成功:

import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, String> env = System.getenv();
        for (String envName : env.keySet()) {
            System.out.format("%s=%s%n", envName, env.get(envName));
        }
    }
}

编译成可执行的 JAR 包:

javac Main.java && \
    jar cfe Main.jar Main Main.class

运行 Java 应用并过滤输出,查看是否包含 OpenTelemetry 相关的环境变量:

java -jar Main.jar | grep -i OTEL

Picked up JAVA_TOOL_OPTIONS: -javaagent:/usr/lib/opentelemetry/javaagent.jar
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
[otel.javaagent 2025-08-30 09:51:14:159 +0000] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.16.0
OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_SERVICE_NAME=my-java-service
OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0,deployment.environment=development
OTEL_METRICS_EXPORTER=otlp
[otel.javaagent 2025-08-30 09:51:22:649 +0000] [OkHttp http://localhost:4317/...] ERROR io.opentelemetry.exporter.internal.grpc.GrpcExporter - Failed to export metrics. The request could not be executed. Error message: Failed to connect to localhost/127.0.0.1:4317
java.net.ConnectException: Failed to connect to localhost/127.0.0.1:4317

请忽略这里的连接错误,因为我们没有运行 OpenTelemetry Collector。关键是看到环境变量已经成功注入,并且 Java Agent 已经被激活。

技术实现深度解析

核心机制:LD_PRELOAD 黑魔法

OpenTelemetry Injector 最巧妙的地方在于利用了 Linux 动态链接器的 LD_PRELOAD 机制。

这个机制允许我们在程序启动前预加载自定义的共享库,从而在不修改程序代码的情况下改变其行为。

关键代码实现

// src/main.c - 核心注入逻辑
extern char *program_invocation_short_name;

#define DOTNET_ENV_VAR_FILE "/etc/opentelemetry/otelinject/dotnet.conf"
#define JAVA_ENV_VAR_FILE "/etc/opentelemetry/otelinject/java.conf"
#define NODEJS_ENV_VAR_FILE "/etc/opentelemetry/otelinject/node.conf"

void __attribute__((constructor)) enter() {
    char *env_var_file;
    if (strcmp("dotnet", program_invocation_short_name) == 0) {
        env_var_file = DOTNET_ENV_VAR_FILE;
    } else if (strcmp("java", program_invocation_short_name) == 0) {
        env_var_file = JAVA_ENV_VAR_FILE;
    } else if (strcmp("node", program_invocation_short_name) == 0) {
        env_var_file = NODEJS_ENV_VAR_FILE;
    } else {
        // 不为不支持的程序注入环境变量
        return;
    }

    FILE *fp = fopen(env_var_file, "r");
    if (fp == NULL) return;

    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        // 跳过注释和空行
        if (buffer[0] == '#' || strnlen(buffer, sizeof(buffer)) == 0) continue;

        char *equals = strchr(buffer, '=');
        if (equals != NULL) {
            *equals = '\0';
            char *key = buffer;
            char *value = equals + 1;
            // 移除换行符
            char *newline = strchr(value, '\n');
            if (newline) *newline = '\0';
            // 检查是否为允许的环境变量(实际代码中有白名单检查)
            // 简化版:假设所有 key 都允许
            setenv(key, value, 0);
        }
    }
    fclose(fp);
}

工作流程详解

  1. 预加载阶段

    • LD_PRELOADlibotelinject.so 加载到进程地址空间
    • 动态链接器建立函数符号表
  2. 初始化阶段

    • 构造函数 __attribute__((constructor))main() 前自动执行
    • 检查当前程序类型(java、node、dotnet)
  3. 配置读取阶段

    • 根据程序类型读取对应的配置文件
    • 解析 key=value 格式的环境变量
  4. 环境变量注入阶段

    • 使用 setenv() 系统调用设置环境变量
    • 白名单机制确保安全性
  5. 代理激活阶段

    • 各语言运行时检测到环境变量
    • 自动加载对应的 OpenTelemetry 代理

LD_PRELOAD 注入流程图

graph TD
    A[程序启动] --> B[动态链接器检查 LD_PRELOAD]
    B --> C[加载 libotelinject.so]
    C --> D[执行构造函数 __attribute__]
    D --> E[检查程序类型<br/>java/node/dotnet]
    E --> F[读取对应配置文件]
    F --> G[解析环境变量<br/>key=value]
    G --> H[使用 setenv 设置环境变量]
    H --> I[语言运行时检测环境变量]
    I --> J[自动加载 OpenTelemetry 代理]
    J --> K[应用正常启动<br/>检测生效]

    style A fill:#e1f5fe
    style K fill:#c8e6c9

配置文件

在 OpenTelemetry Injector 中,初始配置文件位于 /etc/opentelemetry/otelinject/ 目录下,分别对应不同的语言。配置文件内容并不复杂,都是包含了语言相关的基础环境变量配置:

#java.conf
JAVA_TOOL_OPTIONS=-javaagent:/usr/lib/opentelemetry/javaagent.jar
#node.conf
NODE_OPTIONS=-r /usr/lib/opentelemetry/otel-js/node_modules/@opentelemetry-js/otel/instrument
#dotnet.conf
CORECLR_ENABLE_PROFILING=1
CORECLR_PROFILER={918728DD-259F-4A6A-AC2B-B85E1B658318}
CORECLR_PROFILER_PATH=/usr/lib/opentelemetry/dotnet/linux-x64/OpenTelemetry.AutoInstrumentation.Native.so
DOTNET_ADDITIONAL_DEPS=/usr/lib/opentelemetry/dotnet/AdditionalDeps
DOTNET_SHARED_STORE=/usr/lib/opentelemetry/dotnet/store
DOTNET_STARTUP_HOOKS=/usr/lib/opentelemetry/dotnet/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll
OTEL_DOTNET_AUTO_HOME=/usr/lib/opentelemetry/dotnet

但为了 OpenTelemetry Injector 能够正常工作,通常我们还需要更多的环境变量:

  • OTEL_EXPORTER_OTLP_ENDPOINT
  • OTEL_EXPORTER_OTLP_PROTOCOL
  • OTEL_LOGS_EXPORTER
  • OTEL_METRICS_EXPORTER
  • OTEL_RESOURCE_ATTRIBUTES
  • OTEL_SERVICE_NAME

有关这些环境变量和默认值的详细信息,请查看以下内容:

安全设计哲学

Injector 实现了多层安全保护:

  • 环境变量白名单:只允许预定义的环境变量列表
  • 输入验证:严格的格式检查和边界限制
  • 程序名精确匹配:避免对无关进程的意外影响
  • 文件访问控制:配置文件路径固定且权限可控

总结:拥抱零代码可观测性

核心定位

OpenTelemetry Injector 是专为传统 VM 环境设计的零代码观测工具,通过LD_PRELOAD 机制实现系统级自动注入。

独特价值

  • 零代码集成:无需修改 Java、Node.js、.NET 应用源码
  • 系统级自动化:全局配置,一次部署全系统生效
  • 多语言统一:打破语言壁垒,统一观测体验
  • 生产就绪:安全、稳定、经过严格测试

适用场景

  • 企业数据中心传统部署
  • 混合技术栈微服务架构
  • 遗留系统观测升级

重要提醒

不适合容器/K8s 环境,云原生场景请使用 OpenTelemetry Operator。

在这个可观测性成为基础设施核心能力的时代,OpenTelemetry Injector 为传统 VM 环境提供了一条优雅而高效的零代码观测路径。

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus