---
title: "OpenTelemetry と ClickStack を使って NextJS アプリケーションを計装する"
date: "2026-09-25T07:21:31.303Z"
author: "Dale McDiarmid"
category: "Engineering"
excerpt: "OpenTelemetry と ClickStack を使用して NextJS アプリケーションを簡単に計装する方法をご紹介します。"
---

# OpenTelemetry と ClickStack を使って NextJS アプリケーションを計装する

## ClickStackを選ぶ理由

すでにリアルタイム分析の基盤として ClickHouse をご利用中であれば、同じ選択をした仲間が世界中に数多く存在します。ClickHouse は社内外向けの分析を問わず、頼れるデータベースとしての地位を急速に確立しており、[最大級のユーザー向け分析ワークロード](https://clickhouse.com/user-stories?useCase=1)を支えています。従来のデータウェアハウスでは、現代のアプリケーションが求める高同時実行性や低レイテンシクエリに対応しきれないため、非常に多くの企業が ClickHouse への移行を進めています。

しかし、ClickHouse を分析に適した存在にしている高速なスキャン、高い圧縮率、膨大なデータ量の処理能力、そして高カーディナリティなワークロードへの対応力といった強みは、ログ、トレース、メトリクスの処理にもそのまま最適です。さらにオープンソースライセンス、優れた相互運用性、OpenTelemetry へのネイティブ対応 (独自のワイドイベントを保存できる柔軟性も含む) を兼ね備えており、オブザーバビリティの強固な基盤となります。

オブザーバビリティと分析を別々の島として扱うべきではないという考え方は、私たち独自のものではありません。Sierra の Arup Malakar 氏が[最近のブログ](https://clickhouse.com/blog/sierra-observability-analytics)で次のように述べています。

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">「オブザーバビリティと分析を別々の島と考えるのではなく、ClickHouse のような優れたコンピュートエンジンを活用した単一のデータ課題として捉えられるようになれば、本当に素晴らしいことです。結局のところ、すべてはデータであり、あとはそこにアクセスする手段が必要なだけなのです」</p><p style="font-size: 15px;">Sierra、Arup Malakar 氏</p>
</blockquote>

[ClickStack のリリース](https://clickhouse.com/blog/announcing-clickstack-in-clickhouse-cloud)により、リアルタイム分析とオブザーバビリティの統合はかつてないほど容易になりました。ClickStack SDK や任意の OpenTelemetry SDK を使ってアプリケーションを計装するだけで、アプリケーションデータとエラー、パフォーマンスメトリクス、各種課題との関連付けをすべて同じ場所ですぐに開始できます。

本記事では、この手順がいかに簡単であるかを実践的な例で解説します。題材として、1.8 兆行を超えるデータを抱え、週に 150 万件以上のクエリを処理する ClickHouse 駆動の Next.js アプリケーション、[ClickPy](https://clickpy.clickhouse.com/) を計装します。

<div class="my-6"><div class="rounded-lg bg-white/10 p-4"><h3 class="text-inherit undefined Typography_suiTitleh3__JlLe2  group/mdHeader toc-ignore" id="test">ClickStack をダウンロード
</h3><p>面倒なセットアップは不要です。世界最速かつ最高のスケーラビリティを誇るオープンソースのオブザーバビリティスタックを数秒で起動できます。</p><a class=" w-full" target="_self" href="https://clickhouse.com/docs/ja/clickstack/getting-started?loc=blog-o11y-global-cta&amp;utm_source=clickhouse&amp;utm_medium=web&amp;utm_campaign=blog"><button class="styles_button__3smpn mt-6 w-full font-medium" data-type="primary"><span class="flex items-center whitespace-nowrap">今すぐ始める</span></button></a></div></div>

## ClickPy 

私たちが数年前から運用している ClickPy は、Python エコシステムの中核パッケージリポジトリである [PyPI](https://pypi.org/) のダウンロード状況を探索できるプロジェクトです。PyPI では毎日約 20 億件のダウンロードが発生しており、開発者がどのライブラリに依存しているかを知るための豊富なメタデータソースとなっています。

データセットの規模は莫大に拡大しました。本番環境で数年運用した現在、メインテーブルには 2016 年以降の大規模な実際の Python パッケージダウンロードを表す約 1.8 兆行のデータが保存されています。この規模にもかかわらずクエリ応答は 1 秒未満を維持しており、大容量かつ高同時実行のワークロードを難なくこなす ClickHouse の能力を実証しています。

![clickpy.png](https://clickhouse.com/uploads/clickpy_8c6b022d72.png)

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">ClickPy の詳細や内部の仕組みについては、2024年に<a href="https://clickhouse.com/blog/clickpy-one-trillion-rows">1 兆行を達成した際</a>に公開したブログ記事をご覧ください。</p>
</blockquote>

このアプリは、数ページで構成されるシンプルな React + Next.js フロントエンドであり、チャート描画には Apache ECharts を、ClickHouse へのクエリ発行には公式クライアントを用いた Node.js を使用しています。SSR/CSR のハイブリッドモデルを採用しており、サーバー側で ClickHouse からデータを取得して props として渡すため初期ロードが高速で、インタラクティブ性と応答性を高めるためにチャートはクライアント側でレンダリングされます。マテリアライズドビューと賢いテーブル選択のおかげで、数兆行の規模でもパフォーマンスが低下せず、迅速な初回ロード、軽快なチャート描画、スムーズなリアルタイム探索を実現しています。このシンプルさゆえに計装が容易でありながら、実際の問題を浮かび上がらせるのに十分なコンポーネントが揃っているため、優れた検証例となります。今回は、さらなるパフォーマンス向上の余地があるか、また特定のパッケージの特性によってダッシュボードのロードが遅くなっていないかを検証したいと考えました。

## Next.js を OTel と ClickStack で計装する

ClickStack は、データベースとしての ClickHouse、UI としての HyperDX、データ収集としての OpenTelemetry という 3 つの要素を統合したものです。構成はこれだけです。複数のツールを行き来する負担なしに、完全なオブザーバビリティスタックが手に入ります。

![clickstack_arch.png](https://clickhouse.com/uploads/clickstack_arch_7aea262e12.png)

ClickPy における今回の目的では、アプリケーションから**セッションとトレース**を収集することに主眼を置きます。セッションによってユーザーのブラウザ操作をリプレイできる一方、トレースは Vercel 上でホストされている Node.js サーバーサイドコードとブラウザ上で動作する React フロントエンドの双方から取得できます。

計装にはいくつかの選択肢があります。素の OpenTelemetry SDK を直接組み込むことも可能ですが、ClickStack は開発者体験を向上させる独自の SDK も提供しています。たとえば Node.js SDK には例外キャプチャ機能が組み込まれており、実用的なデフォルト設定が最初から用意されているため、わずか数行のコードで簡単に立ち上げられます。フロントエンド側では、ClickStack JavaScript SDK を使うことでブラウザ内のトレース取得をスムーズに開始でき、ユーザーエクスペリエンスの可視化とリプレイを可能にするユーザーセッションに加え、バックエンドとの関連付けも容易に行えます。

![2025-09-04_13-23-35.png](https://clickhouse.com/uploads/2025_09_04_13_23_35_1bf80c88a1.png)

### ClickStack のデプロイ

ClickStack をローカル環境に最も素早くデプロイする方法は、ClickHouse、HyperDX、および OTel コレクターのエンドポイントが含まれたオールインワンイメージを使用することです。

```bash
docker run -p 8080:8080 -p 4317:4317 -p 4318:4318 docker.hyperdx.io/hyperdx/hyperdx-all-in-one
```

これにより、HyperDX がローカルのポート 8080 で公開されます。[http://localhost:8080](http://localhost:8080) の UI にアクセスし、ユーザーを作成してください。すべてのソースが自動的に作成されます。

![login_hyperdx.png](https://clickhouse.com/uploads/login_hyperdx_3c1c247744.png)

#### ClickHouse Cloud

ClickHouse Cloud ユーザー向けには、[HyperDX がプライベートプレビュー](https://clickhouse.com/cloud/clickstack-private-preview)として提供されており、リクエストに応じて有効化できます。HyperDX は任意のサービスに対して起動可能であり、認証は自動的に処理されるため、ユーザーがアカウントを別途作成する必要はありません。

![cloud_clickstack.gif](https://clickhouse.com/uploads/cloud_clickstack_a22ee815a1.gif)

本稿執筆時点では、Cloud 上で取り込みエンドポイントが直接提供されていないため、取り込み処理のために OTel コレクターをローカルで実行する必要があります。以下のコマンドを実行して開始してください。

```bash
curl -O https://raw.githubusercontent.com/ClickHouse/clickhouse-docs/refs/heads/main/docs/use-cases/observability/clickstack/deployment/_snippets/otel-cloud-config.yaml

# modify to your cloud endpoint
export CLICKHOUSE_ENDPOINT=
export CLICKHOUSE_PASSWORD=
# optionally modify 
export CLICKHOUSE_DATABASE=default

# osx
docker run --rm -it \
  -p 4317:4317 -p 4318:4318 \
  -e CLICKHOUSE_ENDPOINT=${CLICKHOUSE_ENDPOINT} \
  -e CLICKHOUSE_USER=default \
  -e CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} \
  -e CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE} \
  --user 0:0 \
  -v "$(pwd)/otel-cloud-config.yaml":/etc/otel/config.yaml \
  -v /var/log:/var/log:ro \
  -v /private/var/log:/private/var/log:ro \
  otel/opentelemetry-collector-contrib:latest \
  --config /etc/otel/config.yaml 
```

コレクターがデプロイされたら、HyperDX の UI に戻り、トレースとセッションのソースを作成します。

![clickstack-sources.gif](https://clickhouse.com/uploads/clickstack_sources_8ffbedf7f1.gif)

### ブラウザ側の計装

今回の [Next.js](https://clickhouse.com/docs/ja/clickstack/ingesting-data/sdks/nextjs) アプリケーションには、クライアント側とサーバー側の両方のコンポーネントがあります。クライアント側には ClickStack Browser SDK を使用します。素の OpenTelemetry SDK (ClickStack と完全な互換性あり) を使うこともできますが、ClickStack JavaScript SDK には大きな利点があります。それはセッションリプレイ機能と、簡単なフラグ設定で有効化できる HTTP リクエストのコンソールログやネットワークキャプチャなどの組み込み機能です。

計装に必要なコードはわずか数行です。まずパッケージをインストールします。

```bash
npm install @hyperdx/browser
```

次に、アプリの起動時に確実に読み込まれる場所で SDK を初期化する必要があります。ClickPy の場合、何よりも先にクッキー同意バナーが表示されるようになっており、[layout.js で追加されています](https://github.com/ClickHouse/clickpy/blob/main/src/app/layout.js)。ここが初期化コードを追加するのに最適な場所です。

```javascript
import HyperDX from '@hyperdx/browser';

HyperDX.init({
 url: process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318',
 apiKey: process.env.NEXT_PUBLIC_HYPERDX_API_KEY || '',
 service: 'clickpy-frontend',
 tracePropagationTargets: [
   /localhost:\d+/i,
   new RegExp(process.env.NEXT_PUBLIC_DOMAIN || 'localhost', 'i')
 ],
 consoleCapture: true,
 advancedNetworkCapture: true,
});
```

ここで注目すべき点がいくつかあります。

* **サービス名** - ブラウザからのスパンには `clickpy-frontend` というタグが付けられ、サーバーで生成されたスパンと簡単に区別できます。
* **OTLP エンドポイント** – デフォルトでは、ローカルの OpenTelemetry コレクター (localhost:4318) にトレースを送信します。Vercel などのプラットフォームにデプロイする場合は、`NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT` 環境変数経由で設定します。
* **API キー** – ローカルコレクターで認証を使用していない場合は不要ですが、パブリックエンドポイントを使用し、ClickPy をインターネット上に公開する場合は必須です。これは `NEXT_PUBLIC_HYPERDX_API_KEY` で処理します。
* **コンソールおよびネットワークキャプチャ** – `consoleCapture` と `advancedNetworkCapture` を有効にすると、[SDK がコンソールログおよびリクエスト/レスポンスの詳細 (ヘッダーやボディを含む) を自動的に記録します](https://clickhouse.com/docs/ja/clickstack/ingesting-data/sdks/browser)。
* **トレース伝播** – ここが重要なポイントです。`tracePropagationTargets` を定義することで、ブラウザからサーバーへのリクエストに `traceparent` ヘッダーが確実に付与されます。これにより、ブラウザで生成されたスパンが同じトレースとしてサーバー側で生成されたスパンとリンクし、**フロントエンドとバックエンドにまたがる完全な分散トレース**が実現します。

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">OpenTelemetry のスパンとは、コンテキストやメタデータとともにキャプチャされる、HTTP リクエストやデータベースクエリなどの作業単位を表す計測された操作です。トレースは複数のスパンの集まりであり、サービス全体にわたるリクエストやワークフローのエンドツーエンドの経路を示します。トレース伝播を利用すると、クライアントとサーバーの双方からのスパン (たとえばユーザーがパッケージページでフィルターを適用した際など) が同じトレースにリンクされ、一連のインタラクションを完全に繋がった状態で把握できます。今回の例では、ユーザーがダッシュボードページで Python パッケージを開いたりフィルターをかけたりする操作がトレースとなり、ClickHouse へのクエリなどの作業単位がスパンとして表されます。</p>
</blockquote>

この設定だけで、ClickPy のすべてのページロード、ユーザーの操作、ブラウザ内のネットワーク呼び出しが ClickStack に直接送信され、サーバー側のトレースやログと関連付ける準備が整います。

### Node.js サーバー側の計装

サーバー側でも同様にいくつかの選択肢があります。Node.js 向けの素の OpenTelemetry、[Vercel の Next.js 向け SDK](https://vercel.com/docs/otel)、または ClickStack Node.js SDK (OTel と完全互換で、便利なデフォルト設定を備えています) です。ClickPy は Vercel 上で動作するため、後者 2 つのいずれかが最も適しています。

この例では、HTTP リクエストとレスポンスのキャプチャを標準で備えている ClickStack SDK を使用します。ClickPy ではサーバーコンポーネントが HTTP 経由で ClickHouse にクエリを送信するため、リクエストボディに観測・分析対象としたい SQL クエリが含まれており、この機能が特に役立ちます。

ここでも対象のパッケージをインストールします。

```bash
npm add @hyperdx/node-opentelemetry
```

今回はやや古いバージョンである Next.js 14 を使用しているため、instrumentation フックを有効にする必要があります。これには、`next.config.js` ファイルで適切なフラグを有効にするだけです。

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">以降のバージョン (Next.js 15 以上) では、この手順は不要です。</p>
</blockquote>

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    instrumentationHook: true,
  },
};

module.exports = nextConfig;
```

これを有効にした上で、プロジェクトルート (構成によっては `/src`) に `instrumentation.js` を作成し、SDK を初期化する `register` 関数を用意します。この関数は、サーバー起動時に Next.js によって呼び出されます。

```javascript
// instrumentation.js
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { initSDK } = await import('@hyperdx/node-opentelemetry');
    initSDK({
      service: 'clickpy',
      // Auto-instruments Next.js + HTTP and captures headers/bodies
      advancedNetworkCapture: true,
      // You can add more instrumentations here if you need them later:
    });
  }
}
```

`service` パラメータにより、サーバー側のスパンにはサービス名 `clickpy` が付与され、バックエンド由来であることが明確になります。`advancedNetworkCapture` を有効にすることで、ClickHouse の SQL クエリを含む HTTP リクエストとレスポンスの全容もキャプチャされるため、パフォーマンスのより深い可視化が可能になります。

SDK の詳細設定は、すべて環境変数を通じて行います。シェルまたは `.env.local` ファイルで 4 つの主要な変数を設定します。

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
HYPERDX_API_KEY=38a8701c-3d72-49f8-8d96-aedaf77d0d55
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST=accept-encoding,host,traceparent,user-agent
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=accept-encoding,host,traceparent,user-agent
```

ここでも、OTLP エンドポイントとオプションの API キーの設定を指定します。2 つのヘッダーキャプチャ用変数では、クライアントおよびサーバーの呼び出しでどのリクエストヘッダーを記録するかを指定します。これにより、authorization などの機密ヘッダーを除外しつつ、traceparent などの有用なコンテキストを確実に保持できます。[リクエストやレスポンスのボディをマスキングする](https://clickhouse.com/docs/ja/clickstack/ingesting-data/sdks/browser)必要がある場合や、レスポンスヘッダーのキャプチャを制限したい場合にも、他のオプションが利用可能です。

### 計装の拡張

各 SDK は標準状態でも充実した計装機能を提供しますが、さらに詳細な情報が必要になる場合もあります。今回のケースでは、クエリをダッシュボード上の視覚コンポーネントと直接紐付けたいと考えました。各コンポーネントには名前とパラメータがあるため、これらもキャプチャします。また、返された行数やクエリ設定とともに、各 ClickHouse リクエスト全体の所要時間も記録したいと考えます。これを実現する最も簡単な方法は、各クエリが発行されるタイミングでカスタムスパンを作成し、クエリ完了時に閉じることです。ClickPy 内のすべてのクエリは、以下に示す単一の [`query` 関数](https://github.com/ClickHouse/clickpy/blob/9f4b4bfd6fd9447f5988671904f50a1cf71e231b/src/utils/clickhouse.js#L766)を経由するため、実装はシンプルです。

```javascript
async function query(query_name, query, query_params) {
    const results = await clickhouse.query({
        query: query,
        query_params: query_params,
        format: 'JSONEachRow',
        clickhouse_settings: getQueryCustomSettings(query_name)
    })
    let query_link = `${process.env.NEXT_PUBLIC_QUERY_LINK_HOST || process.env.CLICKHOUSE_HOST}?query=${base64Encode(query)}`
    if (query_params != undefined) {
        const prefixedParams = Object.fromEntries(
            Object.entries(query_params)
                .filter(([, value]) => value !== undefined)
                .map(([key, value]) => [`param_${key}`, Array.isArray(value) ? `['${value.join("','")}']` : value])
        );
        query_link = `${query_link}&tab=results&${Object.entries(prefixedParams).map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join('&')}`
    }
    return Promise.all([Promise.resolve(query_link), results.json()]);
}
```

カスタムスパンを作成するには、query メソッドの開始時に現在の OpenTelemetry トレース上で `startSpan` を呼び出し、ClickHouse クエリが終了して関数が戻る直前に `end()` でクローズします。その間で、クエリ応答時間などの統計情報をスパン属性としてキャプチャします。コードの主要部分は以下のとおりです (関数全体のコードは[こちら](https://github.com/ClickHouse/clickpy/blob/9f4b4bfd6fd9447f5988671904f50a1cf71e231b/src/utils/clickhouse.js#L766-L826))。

```javascript
xport async function query(query_name, query, query_params) {
  const span = tracer.startSpan(query_name, {
    attributes: {
      'db.system': 'clickhouse',
      // Add a short/obfuscated statement if you want. Full SQL can be large/PII.
      // 'db.statement': truncate(query),
      'db.parameters': truncate(safeJson(query_params ?? {})), // ← your params
    },
  });

  try {
    const start = performance.now();
    // run the query inside the span's context
    const results = await context.with(trace.setSpan(context.active(), span), () =>
      clickhouse.query({
        query,
        query_params,
        format: 'JSONEachRow',
        clickhouse_settings: getQueryCustomSettings(query_name),
      })
    );

    const data = await results.json(); // materialize rows to count
    const end = performance.now();
    // annotate outcome
    if (span.isRecording()) {
      span.setAttribute('db.response_time_ms', Math.round(end - start));
      span.setAttribute('db.rows_returned', Array.isArray(data) ? data.length : 0);
      // attach useful customs
      span.setAttribute('clickhouse.settings', truncate(safeJson(getQueryCustomSettings(query_name))));
    }

    span.setStatus({ code: SpanStatusCode.UNSET });
    span.end();
    return Promise.all([Promise.resolve(query_link), Promise.resolve(data)]);
  } catch (err) {
    if (span.isRecording()) {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message });
    }
    span.end();
    throw err;
  }
}
```

この計装の結果、HTTP リクエストとペイロードを記録する自動キャプチャの `ClickHouse.query` スパンと並ぶ形で、各 ClickHouse クエリごとに追加のカスタムスパンが生成されます。

## データの分析

ClickPy の計装が完了したため、オブザーバビリティデータの収集と探索が可能になりました。アプリ自体は Vercel にデプロイされており、[clickpy.clickhouse.com](https://clickpy.clickhouse.com) で一般公開されています。Vercel へのデプロイには、前述の環境変数を設定する必要があります。

テレメトリを受信するために、SSL 経由で OpenTelemetry コレクターのエンドポイントを実行しています。公開されている ClickPy インスタンスからのデータは、[sql.clickhouse.com](https://sql.clickhouse.com) にあるパブリックデモクラスターの `otel_clickpy` データベースに取り込まれます。ご自身の SQL クエリを使って自由に探索してみてください。また、[play-clickstack.clickhouse.com](https://play-clickstack.clickhouse.com) にある HyperDX のパブリックインスタンスで、このデータを直接可視化することもできます。トレースは `ClickPy Traces`、セッションリプレイは `ClickPy Sessions` というソース名で確認できます。

たとえば、ClickPy を開いて任意のパッケージに移動し、時間でフィルターをかけると、該当するセッションが HyperDX のパブリックインスタンスに表示されます。セッションを再生して各ユーザー操作を確認したり、処理の過程で生成されたトレースやスパンをドリルダウンしたりできます。これにより、ユーザーフローの全体像と各ステップにおける詳細なパフォーマンス内訳の両方が得られ、デバッグ、最適化、実際の利用状況の把握に威力を発揮します。

![clickpy_trace.gif](https://clickhouse.com/uploads/clickpy_trace_e29a5f059d.gif)

カスタム計装のおかげで、各 ClickHouse クエリの詳細を確認し、関連する可視化コンポーネントと発行されたクエリの両方を追跡できている点に注目してください。

![clickpy_query.png](https://clickhouse.com/uploads/clickpy_query_f34ceeb827.png)

セッションリプレイは個別のユーザー問題のデバッグに役立ちますが、データをより大局的に捉えることで傾向を把握し、アプリケーション全体のパフォーマンス向上につながる要素を特定することもできます。

ClickStack の強みの 1 つは柔軟性です。ClickHouse の JSON 型による **Schema-on-Write** と、**Schema-on-Read** の両方をサポートしています。後者は、挿入時にすべてを事前処理しない場合でも、ClickHouse 組み込みの文字列関数を使用してクエリ実行時に生の文字列から値を抽出できるため便利です。

ユーザーがパッケージページに移動するたびに、`documentLoad` スパンがトリガーされます。そのスパンの `location.href` 属性にはパッケージの URL が含まれています。`SpanName: documentLoad` でフィルタリングし、`location.href` からパッケージ名を抽出することで、各ユーザーがアクセスしたパッケージを確認できます。ClickHouse の SQL 関数である [`splitByChar`](https://clickhouse.com/docs/ja/reference/functions/regular-functions/splitting-merging-functions#splitbychar) を利用して URL からパッケージ名を切り出している点に注目してください。これにより、実質的に読み取り時にスキーマを生成しています。

![searching_packages_v2.gif](https://clickhouse.com/uploads/searching_packages_v2_d587ae5e3f.gif)

各 ClickHouse クエリのスパンに可視化コンポーネント名をタグ付けすることで、異なる可視化コンポーネント間のパフォーマンスの推移を簡単に追跡・比較できます。`db.response_time_ms` を参照すれば、可視化タイプごとのクエリコストが分かります。以下では、SpanName (この場合は可視化名) でグループ化し、これを時系列でプロットしています。

![package_ranking_v3.gif](https://clickhouse.com/uploads/package_ranking_v3_ebfd4479d1.gif)

`getPackageRanking` クエリが最も重く、平均して 4 秒以上かかっていることが一目で分かります。このコンポーネントは、総ダウンロード数に関してそのパッケージが同系統の中でどの位置にランク付けされているかを表示するものです。

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">ユーザー体験に基づき、このクエリが最も遅いのではないかと推測していました。このコンポーネントは非同期で読み込まれますが、ページ上の他のコンポーネントと比べてパフォーマンスの差がこれほど顕著であることが客観的に示されたため、パフォーマンス改善の取り組みが始まりました。</p>
</blockquote>

各可視化コンポーネントの時系列パフォーマンスを見ると、他のコンポーネントでもレイテンシが急激に跳ね上がっている箇所が見られます。可視化タイプを確認すると、それらはすべて [clickpy.clickhouse.com](https://clickpy.clickhouse.com) のトップページに由来し、「急上昇 (emerging)」や「注目 (hot)」のパッケージを表示している部分でした。これはサイトのアクティビティとおおむね相関していますが、これらの急増の原因を究明するため、より詳細な調査を行う契機となりました。

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">ClickHouse Cloud サービスは多くの公開デモをホストしているため、クエリ負荷の変動が原因の特定を難しくする側面もあります。また、ユーザーが <a href="https://sql.clickhouse.com">sql.clickhouse.com</a> で公開データセットを探索する際に任意の SQL クエリを実行できる環境でもあります。こうしたレベルでアプリ内部を詳細に把握できた結果、いくつもの最適化作業が開始されました。</p>
</blockquote>

### オブザーバビリティデータとアプリケーションデータの相関

可視化レイヤーとしての HyperDX の枠を超え、他の可視化ツールを使用して、まったく同一のオブザーバビリティデータに対して SQL でより高度な分析を行うこともできます。これは、オブザーバビリティデータを単なるデータ課題の 1 つとして扱うメリットを示す好例です。

以下の例では ClickHouse Cloud の可視化機能を使用していますが、Grafana や Superset などのツールを使っても同様の可視化を簡単に構築できます。

シンプルな問いの例: ***ユーザーが ClickPy で最も頻繁に探索している人気パッケージはどれか？***

ここでも、`location.href` カラムからパッケージを抽出し、`rum.sessionId` の値を使用して過去 12 時間におけるユニークビジター数を特定します。

<pre>
<code clickhouse_settings='{"enable_parallel_replicas":1}'  run='false' type='click-ui' language='sql' runnable='true' view='chart' chart_config='eyJ0eXBlIjoiYmFyIiwiY29uZmlnIjp7InRpdGxlIjoiTW9zdCBwb3B1bGFyIHBhY2thZ2VzIiwieGF4aXMiOiJQYWNrYWdlIiwieWF4aXMiOiJ2aXNpdHMiLCJzdGFjayI6ZmFsc2V9fQ'  play_link='https://sql.clickhouse.com/?query_id=3WGQK7S15SRK1DHHTCCXCA' show_statistics='true'>

SELECT splitByChar('/', SpanAttributes['location.href'])[-1] as Package, count() as visits, uniq(ResourceAttributes['rum.sessionId']) as sessions
FROM otel_clickpy.otel_traces WHERE Package !=''
GROUP BY Package 
ORDER BY visits DESC 
LIMIT 20

</code>
</pre>

上記のシンプルな例は、大半のオブザーバビリティツールでも実現できるでしょう。しかし、さらに踏み込んで、アプリケーションデータとオブザーバビリティデータを相関させたい場合はどうでしょうか。

たとえば、人気の高いパッケージほど ClickPy の動作が目に見えて遅くなっていないか、という疑問を私たちは以前から抱いていました。人気の高いパッケージほど ClickHouse 内の行数が増えるため、直感的には遅くなりそうに思えます。しかし、増分マテリアライズドビューを採用していればその影響は相殺されるはずです。挿入時に集計を計算し、バックグラウンドマージを活用することで、プロジェクトあたりの行数を比較的一定に保てるからです。

<blockquote style="font-size: 16px;">
<p style="font-size: 18px;">増分マテリアライズドビューは、SELECT クエリの結果を永続化することで、計算処理をクエリ実行時から挿入時へとシフトさせます。新しい行が挿入されると定義された SQL クエリが即座に適用され、結果がターゲットテーブルに書き込まれます。重い計算はすでに実行済みであるため、このターゲットに対するクエリは大幅に高速化されます。その後、バックグラウンドマージによって同一の GROUP BY キーに対する部分集計が統合され、長期にわたって正確かつ効率的な結果が保証されます。詳細は<a href="https://clickhouse.com/docs/ja/concepts/features/materialized-views/incremental-materialized-view">ドキュメント</a>をご覧ください。</p>
</blockquote>

この検証には、ダウンロード数の多いパッケージ (約 1.8 兆行のデータへより多く寄与しているパッケージ) に対するクエリが、ダウンロード頻度の低いパッケージに対するクエリよりも遅くなっているかを問うクエリが必要です。ClickPy が適切に設計されていれば、大規模なパッケージであっても体感できるほどの遅延は生じないはずです。

**これに答えるには、オブザーバビリティデータとアプリケーションデータを関連付ける必要があります。**

まず、アプリケーションデータから各パッケージの総ダウンロード数 (`pypi.pypi_downloads_per_day` マテリアライズドビュー経由) を取得します。

<pre>
<code run='false' type='click-ui' language='sql' runnable='true' chart_config='eyJ0eXBlIjoiaG9yaXpvbnRhbCBiYXIiLCJjb25maWciOnsidGl0bGUiOiJMYXRlbmN5IHBlciBwYWNrYWdlIHNpemUiLCJ4YXhpcyI6InByb2plY3QiLCJ5YXhpcyI6InRvdGFsIiwic3RhY2siOmZhbHNlfX0'  play_link='https://sql.clickhouse.com/?query=U0VMRUNUCiAgcHJvamVjdCwKICBzdW0oY291bnQpIEFTIHRvdGFsCkZST00gcHlwaS5weXBpX2Rvd25sb2Fkc19wZXJfZGF5CkdST1VQIEJZIHByb2plY3Q&chart=eyJ0eXBlIjoiaG9yaXpvbnRhbCBiYXIiLCJjb25maWciOnsidGl0bGUiOiJMYXRlbmN5IHBlciBwYWNrYWdlIHNpemUiLCJ4YXhpcyI6InByb2plY3QiLCJ5YXhpcyI6InRvdGFsIiwic3RhY2siOmZhbHNlfX0' show_statistics='true'>

SELECT
  project,
  sum(count) AS total
FROM pypi.pypi_downloads_per_day
GROUP BY project
</code>
</pre>

ClickPy におけるパッケージ単位のパフォーマンスを測定するには、ClickHouse からの平均クエリ応答時間、すなわち `db.response_time_ms` カラムを使用します。パッケージ名はスパン属性内に JSON 文字列として埋め込まれているため、[`JSONExtractString`](https://clickhouse.com/docs/ja/reference/functions/regular-functions/json-functions#jsonextractstring) 関数を用いた Schema-on-Read により抽出します。

<pre>
<code run='false' type='click-ui' language='sql' runnable='true'  chart_config='eyJ0eXBlIjoiYmFyIiwiY29uZmlnIjp7InRpdGxlIjoiTGF0ZW5jeSBwZXIgcGFja2FnZSBzaXplIiwieGF4aXMiOiJwcm9qZWN0IiwieWF4aXMiOiJhdmdfcGVyZm9ybWFuY2UifX0'  play_link='https://sql.clickhouse.com/?query=U0VMRUNUCiAgSlNPTkV4dHJhY3RTdHJpbmcoU3BhbkF0dHJpYnV0ZXNbJ2RiLnBhcmFtZXRlcnMnXSwgJ3BhY2thZ2VfbmFtZScpIEFTIHByb2plY3QsCiAgYXZnKHRvVUludDY0T3JaZXJvKFNwYW5BdHRyaWJ1dGVzWydkYi5yZXNwb25zZV90aW1lX21zJ10pKSBBUyBhdmdfcGVyZm9ybWFuY2UKRlJPTSBvdGVsX2NsaWNrcHkub3RlbF90cmFjZXMKV0hFUkUgSlNPTkV4dHJhY3RTdHJpbmcoU3BhbkF0dHJpYnV0ZXNbJ2RiLnBhcmFtZXRlcnMnXSwgJ3BhY2thZ2VfbmFtZScpICE9ICcnCiAgQU5EIHRvVUludDY0T3JaZXJvKFNwYW5BdHRyaWJ1dGVzWydkYi5yZXNwb25zZV90aW1lX21zJ10pID4gMApHUk9VUCBCWSBwcm9qZWN0Ck9SREVSIEJZIGF2Z19wZXJmb3JtYW5jZSBERVND&chart=eyJ0eXBlIjoiYmFyIiwiY29uZmlnIjp7InRpdGxlIjoiTGF0ZW5jeSBwZXIgcGFja2FnZSBzaXplIiwieGF4aXMiOiJwcm9qZWN0IiwieWF4aXMiOiJhdmdfcGVyZm9ybWFuY2UifX0' show_statistics='true'>

SELECT
  JSONExtractString(SpanAttributes['db.parameters'], 'package_name') AS project,
  avg(toUInt64OrZero(SpanAttributes['db.response_time_ms'])) AS avg_performance
FROM otel_clickpy.otel_traces
WHERE JSONExtractString(SpanAttributes['db.parameters'], 'package_name') != ''
  AND toUInt64OrZero(SpanAttributes['db.response_time_ms']) > 0
GROUP BY project
ORDER BY avg_performance DESC
</code>
</pre>

これら [2 つのクエリ結果セットを `INNER JOIN` で結合し](https://sql.clickhouse.com/?query=LS1NZWFzdXJlIGF2ZXJhZ2UgcmVzcG9uc2UgdGltZSBpbiByZWxhdGlvbiB0byBwYWNrYWdlIHNpemUKU0VMRUNUIAogIGQucHJvamVjdCwgCiAgZC50b3RhbCwKICB0LmF2Z19wZXJmb3JtYW5jZQpGUk9NIChTRUxFQ1QKICBwcm9qZWN0LAogIHN1bShjb3VudCkgQVMgdG90YWwKRlJPTSBweXBpLnB5cGlfZG93bmxvYWRzX3Blcl9kYXkKR1JPVVAgQlkgcHJvamVjdCkgZApJTk5FUiBKT0lOCihTRUxFQ1QKICBKU09ORXh0cmFjdFN0cmluZyhTcGFuQXR0cmlidXRlc1snZGIucGFyYW1ldGVycyddLCAncGFja2FnZV9uYW1lJykgQVMgcHJvamVjdCwKICBhdmcodG9VSW50NjRPclplcm8oU3BhbkF0dHJpYnV0ZXNbJ2RiLnJlc3BvbnNlX3RpbWVfbXMnXSkpIEFTIGF2Z19wZXJmb3JtYW5jZQpGUk9NIG90ZWxfY2xpY2tweS5vdGVsX3RyYWNlcwpXSEVSRSBKU09ORXh0cmFjdFN0cmluZyhTcGFuQXR0cmlidXRlc1snZGIucGFyYW1ldGVycyddLCAncGFja2FnZV9uYW1lJykgIT0gJycKICBBTkQgdG9VSW50NjRPclplcm8oU3BhbkF0dHJpYnV0ZXNbJ2RiLnJlc3BvbnNlX3RpbWVfbXMnXSkgPiAwCkdST1VQIEJZIHByb2plY3QKT1JERVIgQlkgYXZnX3BlcmZvcm1hbmNlIERFU0MpIHQKT04gZC5wcm9qZWN0ID0gdC5wcm9qZWN0Ck9SREVSIGJ5IHJhbmQoKSwgZC50b3RhbCBBU0MgTElNSVQgMTAwMA&chart=eyJ0eXBlIjoic2NhdHRlciIsImNvbmZpZyI6eyJ0aXRsZSI6IkxhdGVuY3kgcGVyIHBhY2thZ2Ugc2l6ZSIsInhheGlzIjoidG90YWwiLCJ5YXhpcyI6ImF2Z19wZXJmb3JtYW5jZSJ9fQ)、ClickHouse Cloud 上で散布図 (X 軸にダウンロード数、Y 軸に平均応答時間) を作成して、人気度とページやクエリのレイテンシとの間に相関関係があるかを視覚的に確認します。設計どおりに機能していれば、左下から右上に線形増加するのではなく、ほぼフラットなトレンドになるはずです。

![2025-09-04_14-51-34.png](https://clickhouse.com/uploads/2025_09_04_14_51_34_375231060e.png)

図に示すように、調査を要するいくつかの例外値を除けば、トレンドはおおむね平坦です。大規模なパッケージであってもパフォーマンスが劣化していないことが確認でき、心強い結果となりました。

最後のステップとして、ClickHouse の統計関数を活用して相関関係を検証します。[`corr`](https://clickhouse.com/docs/ja/reference/functions/aggregate-functions/corr) 関数は 2 つの数値カラム間のピアソンの積率相関係数を計算します。値が 1 であれば強い正の線形相関、-1 であれば強い負の相関、0 であれば無相関を示します。

クエリを拡張し、パッケージのダウンロード数 (合計) と、ClickHouse クエリの平均応答時間および最大応答時間という 2 つのパフォーマンス指標との相関を計算します。

<pre>
<code run='false' type='click-ui' language='sql' runnable='true'  play_link='https://sql.clickhouse.com/?query=U0VMRUNUCiAgICBjb3JyKHRvdGFsLCBtYXhfcGVyZm9ybWFuY2UpLAogICAgY29ycih0b3RhbCwgYXZnX3BlcmZvcm1hbmNlKQpGUk9NCigKICAgIFNFTEVDVAogICAgICAgIGQucHJvamVjdCwKICAgICAgICBkLnRvdGFsLAogICAgICAgIHQubWF4X3BlcmZvcm1hbmNlLAogICAgICAgIHQuYXZnX3BlcmZvcm1hbmNlCiAgICBGUk9NCiAgICAoCiAgICAgICAgU0VMRUNUCiAgICAgICAgICAgIHByb2plY3QsCiAgICAgICAgICAgIHN1bShjb3VudCkgQVMgdG90YWwKICAgICAgICBGUk9NIHB5cGkucHlwaV9kb3dubG9hZHNfcGVyX2RheQogICAgICAgIEdST1VQIEJZIHByb2plY3QKICAgICAgICBPUkRFUiBCWSB0b3RhbCBERVNDCiAgICAgICAgTElNSVQgMTAwMAogICAgKSBBUyBkCiAgICBJTk5FUiBKT0lOCiAgICAoCiAgICAgICAgU0VMRUNUCiAgICAgICAgICAgIEpTT05FeHRyYWN0U3RyaW5nKFNwYW5BdHRyaWJ1dGVzWydkYi5wYXJhbWV0ZXJzJ10sICdwYWNrYWdlX25hbWUnKSBBUyBwcm9qZWN0LAogICAgICAgICAgICBtYXgodG9VSW50NjRPclplcm8oU3BhbkF0dHJpYnV0ZXNbJ2RiLnJlc3BvbnNlX3RpbWVfbXMnXSkpIEFTIG1heF9wZXJmb3JtYW5jZSwKICAgICAgICAgICAgYXZnKHRvVUludDY0T3JaZXJvKFNwYW5BdHRyaWJ1dGVzWydkYi5yZXNwb25zZV90aW1lX21zJ10pKSBBUyBhdmdfcGVyZm9ybWFuY2UsCiAgICAgICAgICAgIHF1YW50aWxlKDAuOSkodG9VSW50NjRPclplcm8oU3BhbkF0dHJpYnV0ZXNbJ2RiLnJlc3BvbnNlX3RpbWVfbXMnXSkpIEFTIHA5MF9wZXJmb3JtYW5jZQogICAgICAgIEZST00gb3RlbF9jbGlja3B5Lm90ZWxfdHJhY2VzCiAgICAgICAgV0hFUkUgKEpTT05FeHRyYWN0U3RyaW5nKFNwYW5BdHRyaWJ1dGVzWydkYi5wYXJhbWV0ZXJzJ10sICdwYWNrYWdlX25hbWUnKSAhPSAnJykgQU5EICh0b1VJbnQ2NE9yWmVybyhTcGFuQXR0cmlidXRlc1snZGIucmVzcG9uc2VfdGltZV9tcyddKSA-IDApCiAgICAgICAgR1JPVVAgQlkgcHJvamVjdAogICAgICAgIE9SREVSIEJZIG1heF9wZXJmb3JtYW5jZSBERVNDCiAgICApIEFTIHQgT04gZC5wcm9qZWN0ID0gdC5wcm9qZWN0CiAgICBPUkRFUiBCWSByYW5kKCksIGQudG90YWwgQVNDCiAgICBMSU1JVCAxMDAwCikK' show_statistics='true'>
SELECT
    corr(total, max_performance),
    corr(total, avg_performance)
FROM
(
    SELECT
        d.project,
        d.total,
        t.max_performance,
        t.avg_performance
    FROM
    (
        SELECT
            project,
            sum(count) AS total
        FROM pypi.pypi_downloads_per_day
        GROUP BY project
        ORDER BY total DESC
        LIMIT 1000
    ) AS d
    INNER JOIN
    (
        SELECT
            JSONExtractString(SpanAttributes['db.parameters'], 'package_name') AS project,
            max(toUInt64OrZero(SpanAttributes['db.response_time_ms'])) AS max_performance,
            avg(toUInt64OrZero(SpanAttributes['db.response_time_ms'])) AS avg_performance,
            quantile(0.9)(toUInt64OrZero(SpanAttributes['db.response_time_ms'])) AS p90_performance
        FROM otel_clickpy.otel_traces
        WHERE (JSONExtractString(SpanAttributes['db.parameters'], 'package_name') != '') AND (toUInt64OrZero(SpanAttributes['db.response_time_ms']) > 0)
        GROUP BY project
        ORDER BY max_performance DESC
    ) AS t ON d.project = t.project
    ORDER BY rand(), d.total ASC
    LIMIT 1000
)

</code>
</pre>

これらの結果から、ダウンロード数と平均または最大の応答時間との間には、ごくわずかな相関しかないことが示唆されます。

<div class="my-6"><div class="rounded-lg bg-white/10 p-4"><h3 class="text-inherit undefined Typography_suiTitleh3__JlLe2  group/mdHeader toc-ignore" id="test">今すぐ ClickStack を試す
</h3><p>コマンド 1 つで、世界最速かつ最高のスケーラビリティを誇るオープンソースのオブザーバビリティスタックをデプロイできます。</p><a class=" w-full" target="_self" href="https://clickhouse.com/docs/ja/clickstack/getting-started?loc=blog-o11y-global-cta&amp;utm_source=clickhouse&amp;utm_medium=web&amp;utm_campaign=blog"><button class="styles_button__3smpn mt-6 w-full font-medium" data-type="primary"><span class="flex items-center whitespace-nowrap">今すぐ始める</span></button></a></div></div>

## まとめ

OpenTelemetry と ClickStack を使ったアプリケーションの計装は、わずか数行のコードで完了するにもかかわらず、パフォーマンスや挙動に関する深い可視性を即座にもたらします。ClickHouse にトレースとセッションリプレイが集約されることで、遅延クエリの特定、異常値の調査、そして実際のユーザー体験のリアルタイムな把握が可能になります。

さらに重要な点として、ClickStack は従来のオブザーバビリティの枠を超えています。分析データとオブザーバビリティデータが同一エンジン内で並んで保持されるため、アプリケーションメトリクス (パッケージのダウンロード数など) とパフォーマンスメトリクス (クエリレイテンシなど) を直接関連付けることができます。この統合された視点によってオブザーバビリティは 1 つのデータ課題へと変貌します。これは[オブザーバビリティコスト最適化プレイブック](/resources/engineering/observability-cost-optimization-playbook)や [Kubernetes オブザーバビリティ実践ガイド](https://clickhouse.com/resources/engineering/mastering-kubernetes-observability-guide)でも詳しく述べているコンセプトであり、ClickStack はまさにこの課題を解決するために作られています。シート課金から脱却し、自社でデータをコントロールしたいチームにとって、強力な [New Relic 代替手段](https://clickhouse.com/resources/engineering/new-relic-alternatives/)となります。

本記事ではトレースに焦点を当て、ログについては扱いませんでした。今後の記事では、ClickStack においてログを収集し、トレースと関連付けることがいかに簡単であり、デバッグや洞察の獲得において同様に強力であるかを紹介する予定です。