opentelemetry之分布式链路追踪 -- 10.openresty agent环境构建
本文从源代码级别做了相应的编译工作,所以,如果有小伙伴想对官方的 nginx instrumentation 进行二次开发或者根据需求自己编译so文件,都可以参考本文的实践过程。
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不是很熟悉,所以在实践过程中碰到了一些问题也曲线跳过去了,如果有大佬知晓解决方案并留言不吝赐教,本人不胜感激。
准备
- 环境
os:ubuntu20.04
一、实践思路
1. 官方方案
根据官方的 instrumentation 的文档,核心的关键是编译出 otel_ngx_module.so
动态链接库,然后 nginx 加载这个动态链接库。官方已经提供了ubuntu/debain 这个动态链接库的下载。
但是官方的动态链接库仅仅支持 Ubuntu 18.04, 20.04, 20.10
、nginx 1.19.8 1.18.0
这几个版本。我们的openresty的os是centos。在集成官方的方案的时候报了几个错误:
-
GLIBC版本过低
这个报错因为我们镜像的os是centos7,内部的CLIBC库的版本比较低,不想对基础镜像的GLIBC做升级操作了,太麻烦。直接换官方ubuntu系统的openresty镜像作为基础镜像。 -
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 需要两个依赖:grpc 和 opentelemetry-cpp。下面描述自己编译 nginx instrumentation 的过程。
- 手动编译grpc参考文档:ubuntu编译grpc
- 手动编译 opentelemetry-cpp: 官方文档
其中用到的核心编译命令如下:cmake -DWITH_OTLP=ON -DgRPC_INSTALL=ON ..
- 手动编译 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
-
修改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]
-
act执行workflow流程
执行命令:$ cd opentelemetry-cpp-contrib $ act -w --reuse
-
获取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
-
修改 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; }
-
执行命令(workflow文件最后一步的命令):
docker buildx build -f test/Dockerfile --target export --output type=local,dest=/tmp/otel_ngx .
最终编译好的so文件在宿主机的 /tmp/otel_ngx目录下。
-
集成到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-contrib的nginx instrumentation的过程。在这个过程中,由于缺乏相应的C++和openresty以及github的actions的知识,所以碰到了很多自己现有的知识水平难以解决的问题。不过,在多天的努力之后,终于还是初步成功的实现了编译,走通了整个流程。
整个过程最大的收获就是:
1,对开源项目如何快速了解和编译运行有了更加熟悉的套路,主要是学会了act工具和了解了github action run。
2,对于如何使用opentelemetry构建apm数据采集系统有了进一步的认识。
3,更熟悉了opentelemetry的架构和jaeger的使用。
但是,此次实践过程也遗留了如下几个问题,希望有了解相关知识的大佬不吝赐教
-
目前编译的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.
-
官方的openresty已经编译了pcre的库,但是为何编译的so文件报错:
最后,文中有任何错误或者改进的地方,欢迎大佬们留言赐教。
更多推荐
所有评论(0)