opentelemetry之分布式链路追踪 – openresty agent环境构建

opentelemetry之分布式链路追踪–.openresty agent环境构建


前言

最近,老板要求在公司的产品中加入链路追踪,之前研究过otel,正好可以实践一番。

公司的产品中有一个gateway服务,这个服务是openresty作为网关并用lua做了一些二次开发。翻看otel的官方文档和github仓库,发现otel有cpp版本的contrib:opentelemetry-cpp-contrib,其中有两个instrumentation:httpd和nginx。众所周知,openresty是一个基于 NGINX 可伸缩的 Web 服务器,所以理论上,如果支持nginx,那么也能支持opnresty。经过了一番折腾,终于曲线实现了otel对openresty的支持。

本文从源代码级别做了相应的编译工作,所以,如果有小伙伴想对官方的 nginx instrumentation 进行二次开发或者根据需求自己编译so文件,都可以参考本文的实践过程。

另外本人对c++的生态以及cmake不是很熟悉,所以在实践过程中碰到了一些问题也曲线跳过去了,如果有大佬知晓解决方案并留言不吝赐教,本人不胜感激。


准备

  1. 环境
    os:ubuntu20.04

一、实践思路

1. 官方方案

根据官方的 instrumentation 的文档,核心的关键是编译出 otel_ngx_module.so动态链接库,然后 nginx 加载这个动态链接库。官方已经提供了ubuntu/debain 这个动态链接库的下载
但是官方的动态链接库仅仅支持 Ubuntu 18.04, 20.04, 20.10nginx 1.19.8 1.18.0 这几个版本。我们的openresty的os是centos。在集成官方的方案的时候报了几个错误:

  1. GLIBC版本过低
    在这里插入图片描述
    这个报错因为我们镜像的os是centos7,内部的CLIBC库的版本比较低,不想对基础镜像的GLIBC做升级操作了,太麻烦。直接换官方ubuntu系统的openresty镜像作为基础镜像。

  2. pcre错误
    在这里插入图片描述
    这个报错是因为官方 instrumentation 实现代码中用到了ngx_regex_exec 函数,具体代码如下:

    // 代码位置:opentelemetry-cpp-contrib/instrumentation/nginx/src/otel_ngx_module.cpp
    #if (NGX_PCRE)
      if (sensitiveHeaderNames)
      {
        int ovector[3];
        if (ngx_regex_exec(sensitiveHeaderNames, &header[i].key, ovector, 0) >= 0)
        {
          sensitiveHeader = true;
        }
      }
      if (sensitiveHeaderValues && !sensitiveHeader)
      {
        int ovector[3];
        if (ngx_regex_exec(sensitiveHeaderValues, &header[i].value, ovector, 0) >= 0)
        {
          sensitiveHeader = true;
        }
      }
    #endif
    
    static bool IsOtelEnabled(ngx_http_request_t *req)
    {
      OtelNgxLocationConf *locConf = GetOtelLocationConf(req);
      if (locConf->enabled)
      {
    #if (NGX_PCRE)
        int ovector[3];
        return locConf->ignore_paths == nullptr || ngx_regex_exec(locConf->ignore_paths, &req->unparsed_uri, ovector, 0) < 0;
    #else
        return true;
    #endif
      }
      else
      {
        return false;
      }
    }
    

    pcre是一个正则表达式的库,作用是让 Nginx 支持 Rewrite 功能。官方的 openresty 的镜像 Dockerfile 中在编译 nginx 的时候包含了这个库的安装和编译,安装和编译的代码如下:

    && curl -fSL https://downloads.sourceforge.net/project/pcre/pcre/${RESTY_PCRE_VERSION}/pcre-${RESTY_PCRE_VERSION}.tar.gz -o pcre-${RESTY_PCRE_VERSION}.tar.gz \
    && echo "${RESTY_PCRE_SHA256}  pcre-${RESTY_PCRE_VERSION}.tar.gz" | shasum -a 256 --check \
    && tar xzf pcre-${RESTY_PCRE_VERSION}.tar.gz \
    && cd /tmp/pcre-${RESTY_PCRE_VERSION} \
    && ./configure \
        --prefix=/usr/local/openresty/pcre \
        --disable-cpp \
        --enable-jit \
        --enable-utf \
        --enable-unicode-properties \
    && make -j${RESTY_J} \
    && make -j${RESTY_J} install \
    

    但是不太清楚为什么otel在引用这个库函数的时候报undefined symbol错误。
    本人对C++以及nginx的编译不是很熟悉,因为时间的关系,先把集成otel的流程跑通,这两段代码暂时注释处理。这就需要重新编译这个库。

2. 自己编译

根据官方的 instrumentation 的文档,编译 nginx instrumentation 需要两个依赖:grpcopentelemetry-cpp。下面描述自己编译 nginx instrumentation 的过程。

  1. 手动编译grpc参考文档:ubuntu编译grpc
  2. 手动编译 opentelemetry-cpp: 官方文档
    其中用到的核心编译命令如下:cmake -DWITH_OTLP=ON -DgRPC_INSTALL=ON ..
  3. 手动编译 nginx instrumentation官方文档
    其中用到的核心编译命令:
    cmake -DCMAKE_INSTALL_PREFIX="/usr/local/grpc" -DCMAKE_INSTALL_PREFIX="/usr/local/opentelemetry-cpp" -DBUILD_SHARED_LIBS=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_DEPS=ON -DgRPC_PROTOBUF_PROVIDER=package -DNGINX_VERSION=1.19.8 ..
    

其中第三步编译失败,失败的原因有好多种,本人对c++代码编译不熟悉,碰到的问题大部分不了解,尝试了多重努力后仍旧失败。
在即将要放弃的时候,无意中搜到了官方的 github action run ci 脚本。之前下载官方github action run 生成的so文件的时候,有过想了解官方整个编译过程的想法,但是对github action run不是很熟悉,所以没有在这个思路上继续往下走。看到了ci文件后立马想到了正确的编译姿势,下面介绍基于官方ci文件编译nginx instrumentation的过程。

二、实践步骤

1.workflow文件

代码如下:

# 文件路径:opentelemetry-cpp-contrib/.github/workflows/nginx.yml

name: nginx instrumentation CI

on:
  push:
    branches: "*"
    paths:
      - 'instrumentation/nginx/**'
      - '.github/workflows/nginx.yml'
  pull_request:
    branches: [ main ]
    paths:
      - 'instrumentation/nginx/**'
      - '.github/workflows/nginx.yml'
jobs:
  nginx-build-test:
    name: nginx
    runs-on: ubuntu-20.04
    strategy:
      matrix:
        os: [ubuntu-21.04, ubuntu-20.04, ubuntu-18.04, debian-10.11]
        nginx-rel: [mainline, stable]
    steps:
      - name: checkout otel nginx
        uses: actions/checkout@v3
      - name: setup
        run: |
          sudo ./instrumentation/nginx/ci/setup_environment.sh
      - name: generate dockerfile
        run: |
          cd instrumentation/nginx/test/instrumentation
          mix local.hex --force --if-missing
          mix local.rebar --force --if-missing
          mix deps.get
          mix dockerfiles .. ${{ matrix.os }}:${{ matrix.nginx-rel }}
      - name: setup buildx
        id: buildx
        uses: docker/setup-buildx-action@master
        with:
          install: true
      - name: cache docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/buildx-cache/
          key: nginx-${{ matrix.os }}-${{ matrix.nginx-rel }}-${{ github.sha }}
          restore-keys: |
            nginx-${{ matrix.os }}-${{ matrix.nginx-rel }}
      - name: build express backend docker
        run: |
          cd instrumentation/nginx
          docker buildx build -t otel-nginx-test/express-backend \
            -f test/backend/simple_express/Dockerfile \
            --cache-from type=local,src=/tmp/buildx-cache/express \
            --cache-to type=local,dest=/tmp/buildx-cache/express-new \
            --load \
            test/backend/simple_express
      - name: build nginx docker
        run: |
          cd instrumentation/nginx
          docker buildx build -t otel-nginx-test/nginx \
            --build-arg image=$(echo ${{ matrix.os }} | sed s/-/:/) \
            -f test/Dockerfile.${{ matrix.os }}.${{ matrix.nginx-rel }} \
            --cache-from type=local,src=/tmp/buildx-cache/nginx \
            --cache-to type=local,dest=/tmp/buildx-cache/nginx-new \
            --load \
            .
      - name: update cache
        run: |
          rm -rf /tmp/buildx-cache/express
          rm -rf /tmp/buildx-cache/nginx
          mv /tmp/buildx-cache/express-new /tmp/buildx-cache/express
          mv /tmp/buildx-cache/nginx-new /tmp/buildx-cache/nginx
      - name: run tests
        run: |
          cd instrumentation/nginx/test/instrumentation
          mix test
      - name: copy artifacts
        id: artifacts
        run: |
          cd instrumentation/nginx
          mkdir -p /tmp/otel_ngx/
          docker buildx build -f test/Dockerfile.${{ matrix.os }}.${{ matrix.nginx-rel}} \
          --target export \
          --cache-from type=local,src=/tmp/.buildx-cache \
          --output type=local,dest=/tmp/otel_ngx .
      - name: upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: otel_ngx_module-${{ matrix.os }}-${{ matrix.nginx-rel }}.so
          path: /tmp/otel_ngx/otel_ngx_module.so

从文件中我们可以看到整个编译的详细过程,最核心的地方是第二步:generate dockerfile

我们无法从 github action run 的编译中提取中间生成的文件,这里要用到一个工具 act,可以在本地模拟github action run的运行过程。这样我们就可以在本地拿到第二步生成的Dockerfile文件。因为act模拟运行整个ci流程时间非常久,为了方便,我们可以对第二步生成的Dockerfile文件单独打包编译,执行完第二步后就kill掉后面的步骤。

2.本地运行act

  1. 修改workflow
    我们不需要编译 debian 和ubuntu 18.04,210.04的版本,所以,修改workflow文件:

    jobs:
      nginx-build-test:
        name: nginx
        runs-on: ubuntu-20.04
        strategy:
          matrix:
            os: [ubuntu-20.04]
            nginx-rel: [mainline]
    
  2. act执行workflow流程
    执行命令:

    $ cd opentelemetry-cpp-contrib
    $ act -w --reuse
    
  3. 获取Dockerfile文件,拷贝到宿主机 opentelemetry-cpp-contrib/instrumentation/nginx/test/ 目录下

    $ docker exec -it act-nginx-instrumentation-CI-nginx bash # act运行过程中会创建编译容器
    $ cd opentelemetry-cpp-contrib/instrumentation/nginx/test
    $ cat Dockerfile.ubuntu-20.04.mainline
    

    Dockerfile文件如下:

    ARG image=ubuntu:20.04
    FROM $image AS build
    
    RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive TZ="Europe/London" \
       apt-get install --no-install-recommends --no-install-suggests -y \
       build-essential autoconf libtool pkg-config ca-certificates gcc g++ git libcurl4-openssl-dev libpcre3-dev gnupg2 lsb-release curl apt-transport-https software-properties-common zlib1g-dev cmake
    
    
    RUN curl -o /etc/apt/trusted.gpg.d/nginx_signing.asc https://nginx.org/keys/nginx_signing.key \
        && apt-add-repository "deb http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" \
        && /bin/bash -c 'echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900"' | tee /etc/apt/preferences.d/99nginx
    
    RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive TZ="Europe/London" \
       apt-get install --no-install-recommends --no-install-suggests -y \
       nginx
    
    RUN git clone --shallow-submodules --depth 1 --recurse-submodules -b v1.36.4 \
      https://github.com/grpc/grpc \
      && cd grpc \
      && mkdir -p cmake/build \
      && cd cmake/build \
      && cmake \
        -DgRPC_INSTALL=ON \
        -DgRPC_BUILD_TESTS=OFF \
        -DCMAKE_INSTALL_PREFIX=/install \
        -DCMAKE_BUILD_TYPE=Release \
        -DgRPC_BUILD_GRPC_NODE_PLUGIN=OFF \
        -DgRPC_BUILD_GRPC_OBJECTIVE_C_PLUGIN=OFF \
        -DgRPC_BUILD_GRPC_PHP_PLUGIN=OFF \
        -DgRPC_BUILD_GRPC_PHP_PLUGIN=OFF \
        -DgRPC_BUILD_GRPC_PYTHON_PLUGIN=OFF \
        -DgRPC_BUILD_GRPC_RUBY_PLUGIN=OFF \
        ../.. \
      && make -j2 \
      && make install
    
    RUN git clone --shallow-submodules --depth 1 --recurse-submodules -b v1.3.0 \
      https://github.com/open-telemetry/opentelemetry-cpp.git \
      && cd opentelemetry-cpp \
      && mkdir build \
      && cd build \
      && cmake -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_INSTALL_PREFIX=/install \
        -DCMAKE_PREFIX_PATH=/install \
        -DWITH_OTLP=ON \
        -DWITH_OTLP_GRPC=ON \
        -DWITH_OTLP_HTTP=OFF \
        -DBUILD_TESTING=OFF \
        -DWITH_EXAMPLES=OFF \
        -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
        .. \
      && make -j2 \
      && make install
    
    RUN mkdir -p otel-nginx/build && mkdir -p otel-nginx/src
    COPY src otel-nginx/src/
    COPY CMakeLists.txt nginx.cmake otel-nginx/
    RUN cd otel-nginx/build \
      && cmake -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_PREFIX_PATH=/install \
        -DCMAKE_INSTALL_PREFIX=/usr/share/nginx/modules \
        .. \
      && make -j2 \
      && make install
    
    FROM scratch AS export
    COPY --from=build /otel-nginx/build/otel_ngx_module.so .
    
    FROM build AS run
    CMD ["/usr/sbin/nginx", "-g", "daemon off;"]
    

3.编译opentelemetry-cpp-contrib并集成到openresty

  1. 修改 opentelemetry-cpp-contrib/instrumentation/nginx/src/otel_ngx_module.cpp 文件

      // #if (NGX_PCRE)
      //       if (sensitiveHeaderNames) {
      //         int ovector[3];
      //         if (ngx_regex_exec(sensitiveHeaderNames, &header[i].key, ovector, 0) >= 0) {
      //           sensitiveHeader = true;
      //         }
      //       }
      //       if (sensitiveHeaderValues && !sensitiveHeader) {
      //         int ovector[3];
      //         if (ngx_regex_exec(sensitiveHeaderValues, &header[i].value, ovector, 0) >= 0) {
      //           sensitiveHeader = true;
      //         }
      //       }
      // #endif
    
      if (locConf->enabled)
      {
        // #if (NGX_PCRE)
        //     int ovector[3];
        //     return locConf->ignore_paths == nullptr || ngx_regex_exec(locConf->ignore_paths, &req->unparsed_uri, ovector, 0) < 0;
        // #else
        return true;
        // #endif
      }
      else
      {
        return false;
      }
    
  2. 执行命令(workflow文件最后一步的命令):
    docker buildx build -f test/Dockerfile --target export --output type=local,dest=/tmp/otel_ngx .

    最终编译好的so文件在宿主机的 /tmp/otel_ngx目录下。

  3. 集成到openresty,参考 opentelemetry-cpp-contrib 官方的集成方案,这里不做赘述。

4.jaeger环境搭建

otel和jaeger集成的时候碰到了一个大坑,jaeger1.21版本后就不再集成otlp的collector,新的版本有自己的jaeger-collector。
也就是说all-in-one启动jaeger后,是不能通过4317的端口向jaeger发送数据的。所以,除了启动 jaeger:all-in-one 容器外,还要启动 otlp-collector 容器作为otlp receiver,并将数据上报给jaeger。
具体的搭建方法可以参考:jaeger,注意,4317端口要暴露出来,修改后的docker-compose如下:

# 代码位置:jaeger/docker-compose/monitor/docker-compose.yml
...
otel_collector:
  networks:
    - backend
  image: otel/opentelemetry-collector-contrib:latest
  volumes:
    - "./otel-collector-config.yml:/etc/otelcol/otel-collector-config.yml"
  command: --config /etc/otelcol/otel-collector-config.yml
  ports:
    - "1888:1888"   # pprof extension
    - "8888:8888"   # Prometheus metrics exposed by the collector
    - "8889:8889"   # Prometheus exporter metrics
    - "13133:13133" # health_check extension
    - "4317:4317"        # OTLP gRPC receiver
    - "55670:55679" # zpages extension
...
# 代码位置:jaeger/docker-compose/monitor/otel-collector-config.yml
receivers:
  jaeger:
    protocols:
      thrift_http:
        endpoint: "0.0.0.0:14278"
  otlp:
    protocols:
      grpc:

  # Dummy receiver that's never used, because a pipeline is required to have one.
  otlp/spanmetrics:
    protocols:
      grpc:
        endpoint: "localhost:65535"

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
  jaeger:
    endpoint: "jaeger:14250"
    tls:
      insecure: true

extensions:
  health_check:
  pprof:
    endpoint: :1888
  zpages:
    endpoint: :55679

processors:
  batch:
  spanmetrics:
    metrics_exporter: prometheus

service:
  extensions: [pprof, zpages, health_check]
  pipelines:
    traces:
      receivers: [jaeger,otlp]
      processors: [spanmetrics, batch]
      exporters: [jaeger]
    # The exporter name in this pipeline must match the spanmetrics.metrics_exporter name.
    # The receiver is just a dummy and never used; added to pass validation requiring at least one receiver in a pipeline.
    metrics/spanmetrics:
      receivers: [otlp/spanmetrics]
      exporters: [prometheus]

...

总结

本篇教程大概讲述了编译 opentelemetry-cpp-contribnginx instrumentation的过程。在这个过程中,由于缺乏相应的C++和openresty以及github的actions的知识,所以碰到了很多自己现有的知识水平难以解决的问题。不过,在多天的努力之后,终于还是初步成功的实现了编译,走通了整个流程。
整个过程最大的收获就是:

1,对开源项目如何快速了解和编译运行有了更加熟悉的套路,主要是学会了act工具和了解了github action run。
2,对于如何使用opentelemetry构建apm数据采集系统有了进一步的认识。
3,更熟悉了opentelemetry的架构和jaeger的使用。

但是,此次实践过程也遗留了如下几个问题,希望有了解相关知识的大佬不吝赐教

  1. 目前编译的so文件otel exporter 不支持jaeger(直连),只支持otlp协议。通过翻看issue,了解到在编译 opentelemetry-cpp 的时候需要添加 -DWITH_JAEGER=ON,同时需要编译依赖thrift。act运行后得到的Dockerfile就变成了如下的样子:

    ...
    RUN git clone --shallow-submodules --depth 1 --recurse-submodules -b v0.14.0 \
      https://github.com/apache/thrift.git \
      && cd thrift \
      && mkdir -p cmake-build \
      && cd cmake-build \
      && cmake -DCMAKE_BUILD_TYPE=Release \
        -DBUILD_COMPILER=ON \
        -DBUILD_CPP=ON \
        -DBUILD_LIBRARIES=ON \
        -DBUILD_NODEJS=OFF \
        -DBUILD_PYTHON=OFF \
        -DBUILD_JAVASCRIPT=OFF \
        -DBUILD_C_GLIB=OFF \
        -DBUILD_JAVA=OFF \
        -DBUILD_TESTING=OFF \
        -DBUILD_TUTORIALS=OFF \
          .. \
       && make -j4 \
       && make install && ldconfig 
    RUN git clone --shallow-submodules --depth 1 --recurse-submodules -b v1.3.0 \
      https://github.com/open-telemetry/opentelemetry-cpp.git \
      && cd opentelemetry-cpp \
      && mkdir build \
      && cd build \
      && cmake -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_INSTALL_PREFIX=/install \
        -DCMAKE_PREFIX_PATH=/install \
        -DWITH_OTLP=ON \
        -DWITH_JAEGER=ON \
        -DWITH_OTLP_GRPC=ON \
        -DWITH_OTLP_HTTP=OFF \
        -DBUILD_TESTING=OFF \
        -DWITH_EXAMPLES=OFF \
        -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
        .. \
      && make -j4 \
      && make install && ldconfig 
    ...
    

    thrif编译命令是参考的 opentelemetry-cpp-contrib/instrumentation/httpd/setup-cmake.sh脚本中的命令。
    然后重新编译,报错如下:
    在这里插入图片描述
    上面明明编译了thrift,为啥这个地方报: Target "otel_ngx_module" links to target "thrift::thrift" but the target was not found.

  2. 官方的openresty已经编译了pcre的库,但是为何编译的so文件报错:
    在这里插入图片描述

最后,文中有任何错误或者改进的地方,欢迎大佬们留言赐教。

Logo

鸿蒙生态一站式服务平台。

更多推荐