使用 Elastic APM 监控你的 C++ 应用程序

158 阅读11分钟

作者:来自 Elastic Haidar Braimaanie

在本文中,我们将使用 Opentelemetry CPP 客户端来监控 Elastic APM 中的 C++ 应用程序。

介绍

开发人员、SRE 和 DevOps 专业人员面临的主要挑战之一是缺乏能够为他们提供应用程序堆栈可见性的综合工具。市场上的许多 APM 解决方案提供了监控基于语言和框架(即 .NET、Java、Python 等)构建的应用程序的方法,但在 C++ 应用程序方面却存在不足。

幸运的是,Elastic 一直是可观察性领域的领先解决方案之一,也是 OpenTelemetry 项目的贡献者。 Elastic 的独特地位及其广泛的可观察性能力使得最终用户能够以多种方式监控用面向对象编程语言和框架构建的应用程序。

在这篇博客中,我们将探索使用 Elastic APM 通过 OpenTelemetry 客户端调查 C++ 跟踪。我们将提供有关如何为 C++ 应用程序实现 OpenTelemetry 客户端并连接到 Elastic APM 解决方案的全面指南。虽然 OTel 有自己的库,并且本博客回顾了如何使用 OTel CPP 库,但 Elastic 也有自己的 OpenTelemetry Elastic Distributions,它是为了提供商业支持而开发的,并且定期完全 upstream。

以下有一些资源可以帮助你入门:

先决条件

  • 环境

        选择环境非常重要,因为对 OTEL 客户端的支持有限。我们尝试使用多种操作系统,并提出以下建议:

  • Ubuntu 22.04
  • Debian 11 Bullseye
  • 在本指南中,我们重点关注 Ubuntu 22.04。
    • 机器:2 vCPU,4GB 就足够了。
    • 图像:Ubuntu 22.04 LTS(x86_64)。
    • 磁盘:~30 GB 就足够了。

实现方法

我们尝试了多种方法,但发现最合适的方法是使用包管理器。经过广泛的测试,尝试运行 otel-cpp 客户端对于用户来说似乎相当具有挑战性。如果从业者希望使用 CMake 和 Bazel 等工具进行构建,这是一个可行的解决方案。因此,当我们测试这两种方法时,很明显我们花费了大部分的时间和精力来修复操作系统的兼容性和依赖性问题,而不是专注于将数据发送到我们的 APM。因此我们决定采用不同的方法。

我们在测试中遇到的主要问题是:

  • 包的兼容性。
  • 包裹的可用性。
  • 库和包的依赖关系。

在本指南中,我们将使用 vcpkg,因为它允许我们引入运行 Opentelemetry C++ 客户端所需的所有依赖项。

安装所需的操作系统工具

更新软件包列表

 sudo apt-get update 

安装 build essentials、cmake、git 和 sqlite 开发库

 sudo apt-get install -y build-essential cmake git curl zip unzip sqlite3 libsqlite3-dev 

sqlite3 和 libsqlite3-dev 允许我们在 C++ 代码中构建/运行 SQLite 查询。

设置 vcpkg

vcpkg 是 C++ 包管理器,我们将使用它来安装 opentelemetry-cpp 客户端。

 1.      # Clone vcpkg
2.      cd ~
3.      git clone https://github.com/microsoft/vcpkg.git
 1.      # Bootstrap
2.      cd ~/vcpkg
3.      ./bootstrap-vcpkg.sh

使用 OTLP gRPC 设置 UInstall OpenTelemetry C++

在本指南中,我们重点介绍将跟踪导出到 Elastic。在撰写本文时,vcpkg 的 opentelemetry-cpp 版本 1.18.0 完全支持跟踪,但直接指标导出受到限制。

安装包

 1.      cd ~/vcpkg
2.      ./vcpkg install opentelemetry-cpp[otlp-grpc]:x64-linux

注意:有时在 Linux 上安装 opentelemetry-cpp 时它不会安装所有必需的软件包。如果遇到这种情况,请尝试再次运行,但传递一个标志以允许不受支持(allow-unsupported):

 ./vcpkg install opentelemetry-cpp[*]:x64-linux --allow-unsupported 

验证

 ./vcpkg list | grep opentelemetry-cpp 

输出应该是这样的:

opentelemetry-cpp:x64-linux 1.18.0 

使用数据库跨度创建 C++ 项目

我们将在 ~/otel-app 中构建一个示例:

  • 使用 SQLite 执行基本的 CREATE/INSERT/SELECT 查询。这有助于展示使用 Elastic APM 上的数据库的应用程序捕获交易。
  • 生成随机跟踪以展示如何在 Elastic APM 上捕获它们。

该应用程序将生成随机查询,其中一些包含数据库事务,一些只是应用程序跟踪。每个查询都包含在一个子跨度中,因此它们在 APM 中显示为单独的数据库事务。

以下是我们项目的结构:

 1.      otel-app/
2.      ├── main.cpp
3.      └── CMakeLists.txt

创建应用项目

 1.      cd ~
2.      mkdir otel-app
3.      cd otel-app

在这个项目中我们将创建两个文件:

  • main.cpp
  • CMakeLists.txt

**请记住,**main.cpp 是你要传递将要向 Elastic 集群发送数据的 otel 导出器(exporters)的地方。因此对于你的技术堆栈来说,它将是你的应用程序的源代码。

示例应用程序代码

 1.      main.cpp
2.      // Below we declare required libraries that we will be using to ship
3.      // traces to Elastic APM
4.      #include <opentelemetry/exporters/otlp/otlp_grpc_exporter.h>
5.      #include <opentelemetry/sdk/trace/tracer_provider.h>
6.      #include <opentelemetry/sdk/trace/simple_processor.h>
7.      #include <opentelemetry/trace/provider.h>

9.      #include <sqlite3.h>
10.      #include <chrono>
11.      #include <iostream>
12.      #include <thread>
13.      #include <cstdlib>  // for rand(), srand()
14.      #include <ctime>    // for time()

16.      // Namespace aliases
17.      namespace trace_api = opentelemetry::trace;
18.      namespace sdktrace  = opentelemetry::sdk::trace;
19.      namespace otlp      = opentelemetry::exporter::otlp;

21.      // Below we are using a helper function to run SQLITE statement inside 
22.      // child span
23.      bool ExecuteSql(sqlite3 *db, const std::string &sql,
24.                      trace_api::Tracer &tracer,
25.                      const std::string &span_name)
26.      {
27.        // Starting the child span
28.        auto db_span = tracer.StartSpan(span_name);
29.        {
30.          auto scope = tracer.WithActiveSpan(db_span);

32.          // Here we mark Database attributes for clarity in APM
33.          db_span->SetAttribute("db.system", "sqlite");
34.          db_span->SetAttribute("db.statement", sql);

36.          char *errMsg = nullptr;
37.          int rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errMsg);
38.          if (rc != SQLITE_OK)
39.          {
40.            db_span->AddEvent("SQLite error: " + std::string(errMsg ? errMsg : "unknown"));
41.            sqlite3_free(errMsg);
42.            db_span->End();
43.            return false;
44.          }
45.          db_span->AddEvent("Query OK");
46.        }
47.        db_span->End();
48.        return true;
49.      }

51.      /**
52.       * DoNonDbWork - Simulate some other operation
53.       */
54.      void DoNonDbWork(trace_api::Tracer &tracer, const std::string &span_name)
55.      {
56.        auto child_span = tracer.StartSpan(span_name);
57.        {
58.          auto scope = tracer.WithActiveSpan(child_span);
59.          // Just sleep or do some "fake" work
60.          std::cout << "[TRACE] Doing non-DB work for " << span_name << "...\n";
61.          std::this_thread::sleep_for(std::chrono::milliseconds(200 + rand() % 300));
62.          child_span->AddEvent("Finished non-DB work");
63.        }
64.        child_span->End();
65.      }

67.      int main()
68.      {
69.        // Seed random generator for example
70.        srand(static_cast<unsigned>(time(nullptr)));

72.        // 1) Create OTLP exporter for traces
73.        otlp::OtlpGrpcExporterOptions opts;
74.        auto exporter = std::make_unique<otlp::OtlpGrpcExporter>(opts);

76.        // 2) Simple Span Processor
77.        auto processor = std::make_unique<sdktrace::SimpleSpanProcessor>(std::move(exporter));

79.        // 3) Tracer Provider
80.        auto sdk_tracer_provider = std::make_shared<sdktrace::TracerProvider>(std::move(processor));
81.        auto tracer = sdk_tracer_provider->GetTracer("my-cpp-multi-app");

83.        // Prepare an in-memory SQLite DB (for random DB usage)
84.        sqlite3 *db = nullptr;
85.        int rc = sqlite3_open(":memory:", &db);
86.        if (rc == SQLITE_OK)
87.        {
88.          // Create a table so we can do inserts/reads
89.          ExecuteSql(db, "CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, info TEXT);",
90.                     *tracer.get(), "db_create_table");
91.        }

93.        // Create the following loop to generate multiple transactions
94.        int num_transactions = 5// Change this variable to the desired number of transaction
95.        for (int i = 1; i <= num_transactions; i++)
96.        {
97.          // Each iteration is a top-level transaction
98.          std::string transaction_name = "transaction_" + std::to_string(i);
99.          auto parent_span = tracer->StartSpan(transaction_name);
100.          {
101.            auto scope = tracer->WithActiveSpan(parent_span);

103.            std::cout << "\n=== Starting " << transaction_name << " ===\n";

105.            // Randomly select whether a transaction will interact with the database or not.
106.            bool doDb = (rand() % 2 == 0); // 50% chance

108.            if (doDb && db)
109.            {
110.              // Insert random data
111.              std::string insert_sql = "INSERT INTO items (info) VALUES ('Item " + std::to_string(i) + "');";
112.              ExecuteSql(db, insert_sql, *tracer.get(), "db_insert_item");

114.              // Select from DB
115.              ExecuteSql(db, "SELECT * FROM items;", *tracer.get(), "db_select_items");
116.            }
117.            else
118.            {
119.              // Do some random non-DB tasks
120.              DoNonDbWork(*tracer.get(), "non_db_task_1");
121.              DoNonDbWork(*tracer.get(), "non_db_task_2");
122.            }

124.            // Sleep a little to simulate transaction time
125.            std::this_thread::sleep_for(std::chrono::milliseconds(200));
126.          }
127.          parent_span->End();
128.        }

130.        // Close DB
131.        sqlite3_close(db);

133.        // Extra sleep to ensure final flush
134.        std::cout << "\n[INFO] Sleeping 5 seconds to allow flush...\n";
135.        std::this_thread::sleep_for(std::chrono::seconds(5));
136.        std::cout << "[INFO] Exiting.\n";
137.        return 0;
138.      }
代码起什么作用?

我们创建 5 个顶级 “transaction_i” 跨度。

对于每笔 transaction,我们随机选择执行 DB 或非 DB 工作



1.  - If DB: Insert a row, then select. Each is a child span.

3.  - If non-DB: We do two “fake tasks” (child spans).


一旦完成后,我们关闭数据库连接并等待 5 秒钟以刷新数据。

示例说明文件

CMakeLists.txt:此文件包含描述源文件和目标的说明。

 1.      cmake_minimum_required(VERSION 3.10)
2.      project(OtelApp VERSION 1.0)

4.      set(CMAKE_CXX_STANDARD 11)
5.      set(CMAKE_CXX_STANDARD_REQUIRED ON)

7.      # Here we are pointing to use the vcpkg toolchain
8.      set(CMAKE_TOOLCHAIN_FILE "PATH-TO/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file")

10.      find_package(opentelemetry-cpp CONFIG REQUIRED)

12.      add_executable(otel_app main.cpp)

14.      # Below we are linking the OTLP gRPC exporter, trace library, and sqlite3
15.      target_link_libraries(otel_app PRIVATE
16.          opentelemetry-cpp::otlp_grpc_exporter
17.          opentelemetry-cpp::trace
18.          sqlite3
19.      )

声明环境变量

在这里,我们将把 Elastic Cloud 端点导出为环境变量

你可以通过执行以下操作来获取此信息:

  • 登录到你的 Elastic Cloud
  • 进入你的 Deployment
  • 在左侧,点击汉堡菜单并向下滚动到 “Integrations”
  • 转到集成内的搜索栏并输入 “APM”

  • 点击 APM 集成
  • 向下滚动并单击最左侧的 OpenTelemetry 选项

  • 你应该能够看到类似于以下屏幕截图的值。复制要导出的值后,单击启动 APM。

 1.      export OTEL_EXPORTER_OTLP_ENDPOINT="APM-ENDPOINT"
2.      export OTEL_EXPORTER_OTLP_HEADERS="KEY"
3.      export OTEL_RESOURCE_ATTRIBUTES="service.

请注意,弹性 OTEL_EXPORTER_OTLP_HEADERS 值通常以 “Authorization=Bearer” 开头,确保将授权中的大写 “A” 转换为小写 “a”。这是因为 otel 标头导出​​器需要小写 “a” 来表示授权。

构建并运行

一旦创建了这两个文件,我们就可以开始构建应用程序。

cd ~/otel-app mkdir -p build cd build

应用程序成功运行后:

./otel-app

你应该能够看到脚本执行并带有类似的控制台输出:

 1.      Console outcome:
2.      === Starting transaction_1 ===
3.      [TRACE] Doing non-DB work for non_db_task_1...
4.      [TRACE] Doing non-DB work for non_db_task_2...

6.      === Starting transaction_2 ===
7.      [TRACE] Doing DB work for doDb_task_1...
8.      [TRACE] Doing DB work for doDb_task_2...

10.      === Starting transaction_3 ===
11.      [TRACE] Doing non-DB work for non_db_task_1...
12.      [TRACE] Doing non-DB work for non_db_task_2...

14.      === Starting transaction_4 ===
15.      [TRACE] Doing non-DB work for non_db_task_1...
16.      [TRACE] Doing non-DB work for non_db_task_2...

18.      === Starting transaction_5 ===
19.      [TRACE] Doing non-DB work for non_db_task_1...
20.      [TRACE] Doing non-DB work for non_db_task_2...

22.      [INFO] Sleeping 5 seconds to allow flush...
23.      [INFO] Exiting.

一旦脚本执行,你应该能够在 Elastic APM 上观察到类似于下面的屏幕截图的跟踪。

在 Elastic APM 中观察

转到 Elastic Cloud,打开你的部署,然后导航到 Observability > APM。

在服务列表中查找应用程序名称(由OTEL_RESOURCE_ATTRIBUTES 定义)。

在该服务的 “Traces” 选项卡中,你会发现多个交易,例如 “transaction_1”,“transaction_2”等等。

展开每个事务会显示子跨度:



1.  - Possibly db_insert_item and db_select_items if random DB path was taken.

3.  - Otherwise, non_db_task_1 and non_db_task_2.


你可以看到有些事务进行 DB 调用,有些则不进行,每个事务都有不同的跨度。

这种多样性展示了你的实际应用可能会产生多种不同的 “routes” 或 “operations”。

Service Map

如果一切运行正常,你应该能够查看你的服务并查看应用程序的服务地图。

Services

My Elastic App

App Transactions

Dependencies

Logs

导航到日志窗口/Discover 以查看传入的应用程序日志:

Patterns

日志模式分析可以帮助你在非结构化日志消息中查找模式,并让你更轻松地检查数据。

最后回顾

以下是我们所做工作的简要总结:

  • 在 Ubuntu 22.04 机器上配置。
  • 安装了 SQLite、dev libs 和 vcpkg 的构建工具。
  • 通过 vcpkg 安装了 opentelemetry-cpp 的客户端。
  • 创建了一个执行应用程序跟踪并捕获数据库操作的最小 C++ 项目。
  • 在 CMakeLists.txt 中连接数据库 sqlite3。
  • 将 Elastic OTLP 端点和令牌作为环境变量导出(小写的 authorization=Bearer key!)。
  • 运行应用程序并在 Elastic APM 中观察数据库交互和应用程序跟踪。
  • 在 Elastic 日志和 Discover 上观察应用程序日志和模式。

常见问题

  • 获取 “Could not find package configuration file provided by opentelemetry-cpp”?

        确保你通过:

-DCMAKE_TOOLCHAIN_FILE=... and -DCMAKE_PREFIX_PATH=... 

传递给 cmake,或者将其嵌入 CMakeLists.txt。

  • Crash: “validate_metadata: INTERNAL:Illegal header key”?

使用全部小写字母

OTEL_EXPORTER_OTLP_HEADERS, e.g. authorization=Bearer \<token>. 
  • Missing otlp_grpc_metrics_exporter.h?

你的 vcpkg 版本的 opentelemetry-cpp (1.18.0) 缺少用于 OTLP 的直接指标导出器。对于指标,要么升级库,要么考虑采用 OpenTelemetry Collector 方法。

  • No data in Elastic APM?

在 APM 中仔细检查你的端点 URL、Bearer token、防火墙规则或服务名称

原文:Monitor your C++ Applications with Elastic APM — Elastic Observability Labs