使用 OpenTelemetry 对 NGINX 进行仪表化

Apache HTTP Server 和 NGINX 是最流行的 Web 服务器。你很可能在应用中使用其中之一。在之前的博客文章中,您学习了如何使用 Apache HTTP Server 的 OpenTelemetry 模块向 Apache HTTP Server 添加可观察性。在这篇博客文章中,您将学习如何为 NGINX 实现可观测性!

安装 NGINX 模块

下面,您将使用 Docker 运行启用和配置了 ngx_http_opentelemetry_module.so 的 NGINX 服务器。当然,您也可以在物理机上使用与下面的 Dockerfile 中使用的相同一组命令来配置 NGINX 服务器。

从一个空目录开始。创建一个名为 Dockerfile 的文件,并将以下内容复制到其中:

FROM nginx:1.23.1 RUN apt-get update ; apt-get install unzip ADD https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/download/webserver%2Fv1.0.3/opentelemetry-webserver-sdk-x64-linux.tgz /opt RUN cd /opt ; unzip opentelemetry-webserver-sdk-x64-linux.tgz.zip; tar xvfz opentelemetry-webserver-sdk-x64-linux.tgz RUN cd /opt/opentelemetry-webserver-sdk; ./install.sh ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/opentelemetry-webserver-sdk/sdk_lib/lib RUN echo "load_module /opt/opentelemetry-webserver-sdk/WebServerModule/Nginx/1.23.1/ngx_http_opentelemetry_module.so;\n$(cat /etc/nginx/nginx.conf)" > /etc/nginx/nginx.conf COPY opentelemetry_module.conf /etc/nginx/conf.d

这个 Dockerfile 的作用如下:

  • 拉取一个带有预安装的 NGINX 1.23.1 的基础图像
  • 安装 unzip
  • 下载 opentelemetry-webserver-sdk-x64-linux
  • 解压缩包,将其放入 /opt,并运行 ./install.sh
  • 将依赖项添加到 /opt/opentelemetry-webserver-sdk/sdk_lib/lib 的库路径 (LD_LIBRARY_PATH)
  • 告诉 NGINX 加载 ngx_http_opentelemetry_module.so
  • 添加模块的配置到 NGINX。

接下来,创建另一个名为 opentelemetry_module.conf 的文件,并将以下内容复制到其中:

NginxModuleEnabled ON; NginxModuleOtelSpanExporter otlp; NginxModuleOtelExporterEndpoint localhost:4317; NginxModuleServiceName DemoService; NginxModuleServiceNamespace DemoServiceNamespace; NginxModuleServiceInstanceId DemoInstanceId; NginxModuleResolveBackends ON; NginxModuleTraceAsError ON;

这将启用 OpenTelemetry,并应用以下配置:

  • 通过 OTLP 将跨度发送到 localhost:4317
  • 将属性 service.name 设置为 DemoServiceservice.namespace 设置为 DemoServiceNamespaceservice.instance_id 设置为 DemoInstanceId
  • 报告追踪日志作为错误,这样您就可以在 NGINX 日志中看到它们

要了解所有可用的设置,请参阅指令的完整列表

有了 Dockerfile 和 NGINX 配置,构建您的 Docker 镜像并运行容器:

docker build -t nginx-otel --platform linux/amd64 . docker run --platform linux/amd64 --rm -p 8080:80 nginx-otel ... 2022/08/12 09:26:42 [error] 69#69: mod_opentelemetry: ngx_http_opentelemetry_init_worker: Initializing Nginx Worker for process with PID: 69

容器启动后,使用例如 curl localhost:8080 发送请求到 NGINX。

由于上面的配置中的 NginxModuleTraceAsError 设置为 ON,您将在 NGINX 的错误日志中看到跟踪记录:

2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: startMonitoringRequest: Starting Request Monitoring for: / HTTP/1.1
Host, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: startMonitoringRequest: WebServer Context: DemoServiceNamespaceDemoServiceDemoInstanceId, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: startMonitoringRequest: Request Monitoring begins successfully , client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_startInteraction: Starting a new module interaction for: ngx_http_realip_module, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_payload_decorator: Key : tracestate, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_payload_decorator: Value : , client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_payload_decorator: Key : baggage, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_payload_decorator: Value : , client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_payload_decorator: Key : traceparent, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_payload_decorator: Value : 00-987932d28550c0a1c0a82db380a075a8-fc0bf2248e93dc42-01, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_startInteraction: Interaction begin successful, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"
2022/08/12 09:31:12 [error] 70#70: *3 mod_opentelemetry: otel_stopInteraction: Stopping the Interaction for: ngx_http_realip_module, client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"

在 Jaeger 中查看跨度

此时,由 NGINX 生成的遥测数据还没有发送到任何一个 OpenTelemetry 收集器或其他可观测性后端。您可以通过创建一个 docker-compose 文件来轻松更改这一点,以启动 NGINX 服务器、收集器和 Jaeger:

创建一个名为 docker-compose.yml 的文件,并添加以下内容:

version: '3.8' services: jaeger: image: jaegertracing/all-in-one:latest ports: - '16686:16686' collector: image: otel/opentelemetry-collector:latest command: ['--config=/etc/otel-collector-config.yaml'] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml nginx: image: nginx-otel volumes: - ./opentelemetry_module.conf:/etc/nginx/conf.d/opentelemetry_module.conf ports: - 8080:80

创建一个名为 otel-collector-config.yaml 的文件,包含以下内容:

receivers: otlp: protocols: grpc: http: exporters: jaeger: endpoint: jaeger:14250 tls: insecure: true service: pipelines: traces: receivers: [otlp] exporters: [jaeger]

在启动容器之前,更新 opentelemetry_module.conf 中的第三行,将导出器的端点修改为正确的值:

NginxModuleEnabled ON; NginxModuleOtelSpanExporter otlp; NginxModuleOtelExporterEndpoint collector:4317;

您无需重新构建 Docker 镜像,因为上述的 docker-compose.yaml 会在容器启动时将 opentelemetry_module.conf 加载为文件容量。

启动和运行所有容器1

docker compose up

在另一个 Shell 中,发送一些请求:

curl localhost:8080

在浏览器中打开 localhost:16686,搜索来自 DemoService 的跨度,并深入查看其中之一。

Jaeger 跟踪视图的屏幕截图,显示了一系列代表不同 NGINX 模块所需的时间的跨度的瀑布图。

您将看到每个 NGINX 模块在请求期间被执行时都会生成一个跨度。通过这些跨度,您可以轻松地发现某些模块的问题,例如,一个离谱的重写。

将 NGINX 放在两个服务之间

当然,NGINX 很少用作独立解决方案!大多数时候它被用作另一个服务前面的反向代理或负载均衡器。而且,可能有一个服务调用 NGINX 来调用下游服务。

向正在运行的示例添加两个附加服务:

  • 一个名为 frontend 的 Node.js 服务,位于前端,并调用 NGINX
  • 一个名为 backend 的 Java 服务,位于 NGINX 后方

更新 docker-compose 文件,将这两个服务添加到其中,并覆盖 NGINX 中的 default.conf

version: '3.8' services: jaeger: image: jaegertracing/all-in-one:latest ports: - '16686:16686' collector: image: otel/opentelemetry-collector:latest command: ['--config=/etc/otel-collector-config.yaml'] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml nginx: image: nginx-otel volumes: - ./opentelemetry_module.conf:/etc/nginx/conf.d/opentelemetry_module.conf - ./default.conf:/etc/nginx/conf.d/default.conf backend: build: ./backend image: backend-with-otel environment: - OTEL_TRACES_EXPORTER=otlp - OTEL_METRICS_EXPORTER=none - OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/ - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf - OTEL_SERVICE_NAME=backend frontend: build: ./frontend image: frontend-with-otel ports: - '8000:8000' environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/ - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf - OTEL_SERVICE_NAME=frontend

创建将请求传递给后端服务的 default.conf

server { listen 80; location / { proxy_pass http://backend:8080; } }

创建两个空文件夹:backendfrontend

frontend 文件夹中创建一个简单的 Node.js 应用程序:

const opentelemetry = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations, } = require('@opentelemetry/auto-instrumentations-node'); const { OTLPTraceExporter, } = require('@opentelemetry/exporter-trace-otlp-http'); const initAndStartSDK = async () => { const sdk = new opentelemetry.NodeSDK({ traceExporter: new OTLPTraceExporter(), instrumentations: [getNodeAutoInstrumentations()], }); await sdk.start(); return sdk; }; const main = async () => { try { const sdk = await initAndStartSDK(); const express = require('express'); const http = require('http'); const app = express(); app.get('/', (_, response) => { const options = { hostname: 'nginx', port: 80, path: '/', method: 'GET', }; const req = http.request(options, (res) => { console.log(`statusCode: ${res.statusCode}`); res.on('data', (d) => { response.send('Hello World'); }); }); req.end(); }); app.listen(8000, () => { console.log('Listening for requests'); }); } catch (error) { console.error('Error occurred:', error); } }; main();

要完成前端服务,创建一个空的 Dockerfile,内容如下:

FROM node:16 WORKDIR /app RUN npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-node express COPY app.js . EXPOSE 8000 CMD [ "node", "app.js" ]

对于后端服务,您将使用安装了 OpenTelemetry Java 代理的 Tomcat。为此,在 backend 文件夹中创建一个如下所示的 Dockerfile

FROM tomcat ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar javaagent.jar ENV JAVA_OPTS="-javaagent:javaagent.jar" CMD ["catalina.sh", "run"]

如您所见,这个 Dockerfile 会自动下载并添加 OpenTelemetry Java 代理。

现在,在顶级目录中应该有以下文件:

  • ./default.conf
  • ./docker-compose.yml
  • ./Dockerfile
  • ./opentelemetry_module.conf
  • ./otel-collector-config.yaml
  • ./backend/Dockerfile
  • ./frontend/Dockerfile
  • ./frontend/app.js

当一切就绪后,您可以启动示例环境1

docker compose up

几秒钟后,您将会看到有五个 Docker 容器正在运行:

  • Jaeger
  • OpenTelemetry Collector
  • NGINX
  • 前端
  • 后端

使用 curl localhost:8000 向前端发送一些请求,然后在浏览器中检查 Jaeger UI 的 localhost:16686。您将看到跟踪从前端到 NGINX 再到后端的过程。

由于 NGINX 正在转发来自 Tomcat 的 找不到页面,因此前端跟踪应该显示一个错误。

Jaeger 中的跟踪视图截图,显示从前端到 NGINX 再到后端的跨度的瀑布图。

下一步是什么?

现在,您应该能够将本篇博客文章中所学的内容应用到您自己的 NGINX 安装中。我们非常乐意听听您的经验!如果遇到任何问题,请创建一个问题


  1. docker-compose is deprecated. For details, see Migrate to Compose V2↩︎ ↩︎