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不是很熟悉,所以在实践过程中碰到了一些问题也曲线跳过去了,如果有大佬知晓解决方案并留言不吝赐教,本人不胜感激。
根据官方的 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的流程跑通,这两段代码暂时注释处理。这就需要重新编译这个库。
根据官方的 instrumentation 的文档,编译 nginx instrumentation 需要两个依赖:grpc 和 opentelemetry-cpp。下面描述自己编译 nginx instrumentation 的过程。
cmake -DWITH_OTLP=ON -DgRPC_INSTALL=ON ..
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的过程。
代码如下:
# 文件路径: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掉后面的步骤。
修改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;"]
修改 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 官方的集成方案,这里不做赘述。
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.
最后,文中有任何错误或者改进的地方,欢迎大佬们留言赐教。