🐥

C++アプリケーションのオブザーバビリティを実現する - OpenTelemetryとNew Relicの活用法

2024/12/22に公開

こんにちは。

これは、New Relic Advent Calendar 2024 シリーズ1 22日目のエントリーです。

ここでは、IoTデバイスなどC++で開発されたソフトウェアやデバイスでもNew Relicを利用してオブザーバービリティ(可観測性)を実現する方法を解説します。

IoTデバイスに対してオブザーバービリティを実現するにはどうしたらよいのでしょうか?

New RelicにおけるC/C++のサポート状況

New RelicのC/C++のサポート状況をみていきます。New Relicに昔から馴染みがある方であれば「New RelicってC SDKを提供していたよね?」と思った方もいるでしょう。それは確かにその通りですが、実はNew RelicのC SDKは2022年4月にサポートが打ち切られています

「もうNew RelicではC言語を利用したアプリケーションに対して利用できないのか...」というと、そういう訳でもありません。New RelicはOpenTelemetryをサポートしており、OpenTelemetryではC++ SDKを提供しています。そのため、今回のエントリーではOpenTelemetryを利用しているのです。

OpenTelemetryで提供されているSDKはC++なので、C言語から直接呼びだすことはできませんが、ラッパー関数などを用意することでC言語からも呼びだすことはできます。

OpenTelemetryとは

OpenTelemetryは、さまざまなアプリケーションのオブザーバービリティを実現するためのオープンソースのフレームワークとツールキットです。主要な機能として、分散システム間のリクエストを追跡し、パフォーマンスのボトルネックやエラーの原因を特定できるトレース機能を提供しています。また、システムリソースの使用状況やカスタムメトリクスを収集し、リアルタイムでパフォーマンスを監視するメトリクス機能も備えています。さらに、アプリケーションログやエラーログ、システムイベントを統合的に管理するログ機能も実装されています。

これらの機能は言語に依存せず実装可能で、標準化されたSDKが提供されています。また、複数のバックエンドシステムとの連携が可能で、拡張性の高いアーキテクチャを採用しています。

OpenTelemetryを利用するメリット

OpenTelemetryを利用する最大のメリットは、幅広いオブザーバービリティツールのサポートと幅広い言語サポートにあります。New Relicが直接対応していない言語でもデータ収集が可能です。C++の他、C#、Go、Java、JavaScript、Erlang、Swift、Rustといった多様なプログラミング言語に対応しています。これらの中にはNew Relicがサポートしている言語もありますが、OpenTelemetryを利用することでNew Relic以外のオブザーバービリティツールにも適用可能となります(小声)。

将来性の観点からも、活発なコミュニティによる継続的な改善や、新しい技術やプラットフォームへの迅速な対応が期待でき、業界標準としての地位を確立しつつあります。

New RelicにおけるOpenTelemetryの利用については、次の記事も参考になります。

C++のソースコードに組み込んでみた

(私を含めて)OpenTelemetryのライブラリをC++から扱うにはどうしたらいいんだ?と思っている方もいるでしょう。ここではOpenTelemetryのC++ SDKのビルドから始め、実際にアプリケーションに組み込みデータ転送ができるアプリケーションを作成するところまで重点的に解説します。

それ以降のOpenTelemetryをNewRelicにて監視する流れは他の言語でも共通ですので、先のリンク等を参考にしてください。

開発環境

開発環境は次の通りです。

今回はmacOSを利用して開発していますが、LinuxやWindowsであってもコンパイラなどの開発環境さえ整えば同じ手順でビルドできます。

  • macOS Sequoia 15.1.1
  • OpenTelemetry C++ SDK 1.18.0

ビルドにはclangとcmakeが必要です。Xcodeをインストールしておきます。cmakeはhomebrewでインストールできます。

brew install cmake

OpenTelemetry C++ SDKのビルドから、アプリに組み込むまで

OpenTelemetryのC++ SDKを含むソースコードを取得して、実際のアプリケーションに組み込むところまでを進めていきます。基本的な流れはGetting Started | OpenTelemetryに書かれているので、基本的にはその流れで進めていきますが、いくつか変更も加えている箇所もあるので、その点も解説しながら進めていきます。

今回作成するプロジェクトのディレクトリ構成は次のようになります。

~/otel-cpp-starter   ← プロジェクトのディレクトリ
│
├── oatpp              ← C++ Webフレームワーク
├── opentelemetry-cpp  ← OpenTelemetry for C++
└── roll-dice          ← サンプルとなるWebアプリケーション

実際に進めていきましょう。まずは、otel-cpp-starterディレクトリを作成します。

cd ~
mkdir -p otel-cpp-starter

oatpp(C++ Webフレームワーク)の準備

C++ Webフレームワークであるoatppを用意します。oatppのヘッダファイルやライブラリをインストールしますが、ここでは$HOME/localディレクトリにインストールするように設定してます。

次のコマンドを実行します。

cd otel-cpp-starter
git clone https://github.com/oatpp/oatpp.git
cd oatpp
git checkout 1.3.0-latest
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/local 
make 
make install

OpenTelemetry C++ SDKの準備

次にOpenTelemetryのC++ SDKをビルドします。

次のコマンドを実行します。

cd ~/otel-cpp-starter
git clone https://github.com/open-telemetry/opentelemetry-cpp.git
cd opentelemetry-cpp

macOSのClangでビルドする場合、CMakeLists.txtファイルに次のC++14でビルドする指定をするオプションを追加します。MacOSのClangは、デフォルトではC++14のソースコードはビルドできないため、明示的にC++14対応のソースコードがビルドできる機能を有効化します。

変更箇所は次の通りです。

$ git diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bf093f2c..5f57f426 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -278,6 +278,7 @@ endif(WIN32)
 # Do not convert deprecated message to error
 if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
   add_compile_options(-Wno-error=deprecated-declarations)
+  add_compile_options(-std=c++14)
 endif()

追加後、ビルドします。

mkdir build 
cd build
cmake -DBUILD_TESTING=OFF ..
cmake --build .
cmake --install . --prefix ../../otel-cpp

サンプルのWebアプリケーションを用意する

今回Tracerの情報を送信するWebアプリケーションを作成します(処理内容としてはサイコロの目を出力するプログラムです)。アプリケーション名はroll-diceです。

では作成しましょう。次のように作業用ディレクトリを作成します。

cd ~/otel-cpp-starter
mkdir roll-dice
cd roll-dice
touch CMakeLists.txt main.cpp

作成したCMakeLists.txtは次の通りです。

コメントを読むと理解できますが、oatppのディレクトリを指定している他、OpenTelemetryのディレクトリも指定しています。最後の行でOpenTelemetry C++ SDKのヘッダファイルやライブラリを指定しています。

cmake_minimum_required(VERSION 3.25)
project(RollDiceServer)
# Set C++ standard (e.g., C++17)
set(CMAKE_CXX_STANDARD 17)
set(project_name roll-dice-server)

# Define your project's source files
set(SOURCES
    main.cpp  # Add your source files here
)
# Create an executable target
add_executable(dice-server ${SOURCES})

set(OATPP_ROOT ../oatpp)
set(opentelemetry-cpp_DIR ../otel-cpp/lib/cmake/opentelemetry-cpp)
find_library(OATPP_LIB NAMES liboatpp.a HINTS "${OATPP_ROOT}/build/src/" NO_DEFAULT_PATH)
if (NOT OATPP_LIB)
  message(SEND_ERROR "Did not find oatpp library ${OATPP_ROOT}/build/src")
endif()
# set the path to the directory containing "oatpp" package configuration files
include_directories(${OATPP_ROOT}/src)

# Use find_package to include OpenTelemetry C++
find_package(opentelemetry-cpp CONFIG REQUIRED NO_DEFAULT_PATH)

# Link against each OpenTelemetry C++ library
target_link_libraries(dice-server PRIVATE
                      ${OATPP_LIB}
                      ${OPENTELEMETRY_CPP_LIBRARIES})

今回作成するWebアプリケーションのソースコードは次の通りです。

このソースコードは、main.cppファイルに格納します。すでにOpenTelemetryのTracerを活用した処理も含まれています。関連する処理にはコメントもいれています。

#include "oatpp/web/server/HttpConnectionHandler.hpp"
#include "oatpp/network/Server.hpp"
#include "oatpp/network/tcp/server/ConnectionProvider.hpp"

// [OpenTelemetry]各種ヘッダファイル
#include "opentelemetry/exporters/ostream/span_exporter_factory.h"
#include "opentelemetry/sdk/trace/exporter.h"
#include "opentelemetry/sdk/trace/processor.h"
#include "opentelemetry/sdk/trace/simple_processor_factory.h"
#include "opentelemetry/sdk/trace/tracer_provider_factory.h"
#include "opentelemetry/trace/provider.h"

#include <cstdlib>
#include <ctime>
#include <string>

using namespace std;

// [OpenTelemetry]名前空間の定義
namespace trace_api = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace trace_exporter = opentelemetry::exporter::trace;

namespace {
  // [OpenTelemetry]Tracerの初期化
  void InitTracer() {
    auto exporter  = trace_exporter::OStreamSpanExporterFactory::Create();
    auto processor = trace_sdk::SimpleSpanProcessorFactory::Create(std::move(exporter));
    std::shared_ptr<opentelemetry::trace::TracerProvider> provider =
      trace_sdk::TracerProviderFactory::Create(std::move(processor));
    //set the global trace provider
    trace_api::Provider::SetTracerProvider(provider);
  }
 
  // [OpenTelemetry]Tracerの終了
  void CleanupTracer() {
    std::shared_ptr<opentelemetry::trace::TracerProvider> none;
    trace_api::Provider::SetTracerProvider(none);
  }

}

class Handler : public oatpp::web::server::HttpRequestHandler {
public:
  shared_ptr<OutgoingResponse> handle(const shared_ptr<IncomingRequest>& request) override {
    auto tracer = opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("my-app-tracer"); // [OpenTelemetry]Tracerの取得
    auto span = tracer->StartSpan("RollDiceServer"); // [OpenTelemetry]Span開始
    int low = 1;
    int high = 7;
    int random = rand() % (high - low) + low;
    // Convert a std::string to oatpp::String
    const string response = to_string(random);
    span->End();                                     // [OpenTelemetry]Spanの終了
    return ResponseFactory::createResponse(Status::CODE_200, response.c_str());
  }
};

void run() {
  auto router = oatpp::web::server::HttpRouter::createShared();
  router->route("GET", "/rolldice", std::make_shared<Handler>());
  auto connectionHandler = oatpp::web::server::HttpConnectionHandler::createShared(router);
  auto connectionProvider = oatpp::network::tcp::server::ConnectionProvider::createShared({"localhost", 8080, oatpp::network::Address::IP_4});
  oatpp::network::Server server(connectionProvider, connectionHandler);
  OATPP_LOGI("Dice Server", "Server running on port %s", static_cast<const char*>(connectionProvider->getProperty("port").getData()));
  server.run();
}

int main() {
  oatpp::base::Environment::init();
  InitTracer(); // [OpenTelemetry]Tracerの初期化
  srand((int)time(0));
  run();
  oatpp::base::Environment::destroy();
  CleanupTracer(); // [OpenTelemetry]Tracerの終了
  return 0;
}

このソースコードをビルドします。

mkdir build
cd build
cmake ..
cmake --build .

そして、実行します。

./dice-server

実行した後、http://localhost:8080/rolldice へアクセスするとサイコロの数値が表示されます。そして、Tracerで送信されるデータが表示されます。以下が出力の一例です。

 I |2024-12-21 23:06:28 1734789988157798| Dice Server:Server running on port 8080
{
  name          : RollDiceServer
  trace_id      : df1e167eb54de41c33dbff74e2e66fff
  span_id       : 8d73f49d4c180535
  tracestate    :
  parent_span_id: 0000000000000000
  start         : 1734789997065296000
  duration      : 4125
  description   :
  span kind     : Internal
  status        : Unset
  attributes    :
  events        :
  links         :
  resources     :
	telemetry.sdk.version: 1.18.0
	telemetry.sdk.name: opentelemetry
	telemetry.sdk.language: cpp
	service.name: unknown_service
  instr-lib     : my-app-tracer
}

まとめ

このようにしてC/C++で開発されたアプリケーションであっても対応可能です。今回はTracerを紹介しましたが、OpenTelemetryのリポジトリにはその他沢山のサンプルコードも含まれていますので、それらについても見てみると何ができるのか理解が深まるでしょう。

明日は、chacco38さんです。よろしくお願いします。

Discussion