Haskell で Open Telemetry を利用してオブザーバビリティーを向上させよう

Open Telemetry って何?

この記事では Open Telemetry のトレースの機能を使います。トレースを使うと、サーバーを越境してコールグラフとその実行時間などを取得することができます。下の画像は Jaeger のスクリーンショットです。Jaeger は Open Telemetry の規格にのっとったコレクター実装のひとつです。

この例では HTTP サーバーと HTTP クライアントでトレースを取得しています。まずサーバーが /1 のパスでリクエストを受けつけたことが分かります。このリクエストに対してレスポンスを返すまでに 845μs かかっていますね。このトレースにおけるひとつの区間をスパンといいます。

次にサーバーはこのリクエストに対して処理をする途中で localhost:7777/2 に HTTP リクエストを投げたことが分かります。リクエストを投げてレスポンスが返ってくるまでに 729μs かかっています。

最後に /2 へのリクエストに対してサーバーが応答したスパンが記録されています。

この例では便宜上、サーバーは自分に対して再度リクエストをしていますが、これは物理的なサーバーが別であっても同様にトレースが取得できます。

Haskell のプログラムに対してトレースを記録したい

Open Telemetry はプログラミング言語や OS などに依存しない仕様ですから、Haskell でもトレースを記録したいです。そうすれば Istio や Node などのスパンとつながったトレースを見ることができます。Haskell では hs-opentelemetry ライブラリーを使用します。

github.com

自分もいっぱいコントリビュートしています。HERP 社からの委託を受け開発しています。

インターフェースは今後破壊的変更が入る可能性が多分にありますが、HERP 社で本番運用している程度に完成しています。

hs-opentelemetry の使い方

hs-opentelemetry はいくつかのパッケージに分かれています。まず基本となるものは hs-opentelemetry-api と hs-opentelemetry-sdk です。apisdk に分かれているのは Open Telemetry の仕様が分けるよう指示しているためであまり意味はありません。トレースを取得するためのトレーサーおよびトレーサーを作成するためのトレーサープロバイダーを作成するために使用します。また「ここからここまでスパンを取得する」というように手動で指定する場合に使用します。手動で指定するには下記の型をもつ inSpan 関数を使用します。

module OpenTelemetry.Trace.Core

…

inSpan ::
  (MonadUnliftIO m, HasCallStack) =>
  Tracer ->
  -- | The name of the span. This may be updated later via 'updateName'
  Text ->
  -- | Additional options for creating the span, such as 'SpanKind',
  -- span links, starting attributes, etc.
  SpanArguments ->
  -- | The action to perform. 'inSpan' will record the time spent on the
  -- action without forcing strict evaluation of the result. Any uncaught
  -- exceptions will be recorded and rethrown.
  m a ->
  m a

inSpan の第4引数の所要時間をスパンとして記録します。

これでスパンは記録できますが、全部を inSpan で書いていくのはいささか邪魔くさいです。そこでインスツルメンテーションが用意されています。初めのトレースの例では wai 用のインスツルメンテーションと http-client インスツルメンテーションを使用しています。インスツルメンテーションを使用すると初めのトレースの例の実装は下のようになります。

{-# LANGUAGE OverloadedStrings #-}

import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types.Status as H
import qualified Network.Wai as W
import qualified Network.Wai.Handler.Warp as W
-- Network.HTTP.Client の代わりに he-opentelemetry のインスツルメンテーションを使用する
import OpenTelemetry.Instrumentation.HttpClient (
  Manager (),
  defaultManagerSettings,
  httpLbs,
  newManager,
 )
-- he-opentelemetry のインスツルメンテーションで提供される WAI ミドルウェアを使用する
import OpenTelemetry.Instrumentation.Wai (newOpenTelemetryWaiMiddleware)
import OpenTelemetry.Trace (
  initializeTracerProvider,
  setGlobalTracerProvider,
 )


main :: IO ()
main = do
  -- デフォルト設定でトレーサープロバイダーを作成する
  tracerProvider <- initializeTracerProvider
  -- グローバルな IORef に作成したトレーサープロバイダーを参照させる
  setGlobalTracerProvider tracerProvider
  -- トレースが取れるようラップされた http-client を作成する
  httpClient <- newManager defaultManagerSettings
  -- トレースを取得する WAI ミドルウェアを作成する
  tracerMiddleware <- newOpenTelemetryWaiMiddleware
  W.run 7777 $ tracerMiddleware $ app httpClient


app :: Manager -> W.Application
app httpManager req res =
  case W.pathInfo req of
    ["1"] -> do
      newReq <- H.parseRequest "http://localhost:7777/2"
      newRes <- httpLbs newReq httpManager
      res $ W.responseLBS H.ok200 [] $ "1 (" <> H.responseBody newRes <> ")"
    ["2"] -> res $ W.responseLBS H.ok200 [] "2"
    _ -> res $ W.responseLBS H.ok200 [] "other"

app 関数はこれまで通りの書きごこちですが、HTTP リクエストを受けてレスポンスを返すまで、HTTP リクエストを投げてレスポンスを受けるまでのスパンが取得できるようになっています。簡単ですね。

インスツルメンテーションには他にも mysql-simple 版や grpc-haskell 版などが用意されています(というか作成しました)。また Datadog 仕様のトレースと接続するためにプロパゲーターなども用意されています(これも作成しました)。

実際に手元で動かしてみたい場合はリポジトリーの examples ディレクトリーを参照してください。

Open Telemetry を活用してオブザーバビリティーを上げていきましょう。

それではメリークリスマス!


これは Haskell アドベントカレンダー 2023 25日目の記事です。

qiita.com