Spring BootとGridDBを使ったリアルタイムイベントトラッキング

イベントトラッキングサービスの定義

今日のデジタル環境において、ユーザーイベントを追跡・分析することは、ユーザー行動に関する洞察を得たり、パフォーマンスを最適化したり、パーソナライズされたユーザー体験を提供するために極めて重要です。

イベントとは、ウェブサイトやモバイルアプリ上でユーザーのアクションをトリガーとして発生するインタラクションのことです。これには、ボタンのクリックからニュースレターへの登録、製品の購入まで、あらゆるものが含まれます。イベントページビュー自体がイベントです。

Event Tracking Serviceを利用することで、企業はイベントが発生するとそれを捕捉して処理することができ、最新のインサイトを得ることができます。これにより、企業は新たなトレンドに迅速に対応し、リアルタイムで問題を特定し、データ駆動型の意思決定を即座に行うことができます。

この記事では、Spring BootとGridDBを使ってリアルタイムイベント追跡サービスを実装する方法を紹介します。

機能要件

技術的な詳細に入る前に、Event Tracking Serviceの主要な機能要件の概要を説明します。これらの要件は、システムの設計と実装を形作ります。

  • リアルタイムでイベントを捕捉し、保存する。
  • 集計されたデータをダッシュボードで提供すること。

容量の見積もり

毎日どれだけのデータを処理する必要があるのかを調べてみましょう。

1日のアクティブユーザーが10K人いるとします。

  • 各ユーザーは1日あたり50クリック * 10K * 50 = 500K clicks per day * 1クリックのデータは0.1KBです。

  • 1日に必要なストレージ 1日に必要なストレージ:0.1KB * 500K = 50メガバイト * 1日あたり

正しいデータベースの選択: SQLかNoSQLか

データベースに関しては、SQLとNoSQLのどちらを選択するかは、アプリケーションの特定の要件に依存します。スピードとスケーラビリティが重要なリアルタイム・イベント追跡サービスでは、NoSQLアーキテクチャが従来のSQLデータベースを凌駕します。

GridDBのようなNoSQLデータベースは、柔軟なスキーマ設計、水平方向のスケーラビリティ、大容量のリアルタイム・イベント・データを処理するための最適化されたパフォーマンスを提供します。GridDBのようなNoSQLデータベースは、柔軟なスキーマ設計、水平スケーラビリティ、そして大容量のリアルタイム・イベント・データを扱うための最適化されたパフォーマンスを提供します。

Spring Bootを使ったJavaアプリケーションのセットアップ

Spring Bootでの実装の詳細に飛び込む前に、Spring Bootフレームワークの基本的な理解を持つことが重要です。Spring Bootは人気のあるJavaベースのフレームワークで、スタンドアロンでプロダクショングレードのSpringベースのアプリケーションの開発を簡素化します。開発プロセスを効率化するツールやライブラリの包括的なセットを提供するので、開発者は定型的なコードを扱うのではなく、ビジネスロジックを書くことに集中できます。

それでは、Spring Bootを使ってJavaアプリケーションのセットアップを進めましょう。

  • Java Development Kit (JDK)をインストールし、開発環境をセットアップします。
  • 好みのビルドツール(MavenやGradleなど)を使って、新しいSpring Bootプロジェクトをセットアップします。https://start.spring.io/ を使ってSpring Bootプロジェクトを初期化します。
  • GridDBやその他の必要なライブラリについて、Spring Bootの依存関係を設定します。このプロジェクトの主なライブラリは以下の通り:

1. spring-boot-starter-web : Spring MVCを使ってRESTfulを含むWebアプリケーションを構築するためのスターター。デフォルトの組み込みコンテナとしてTomcatを使用します。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

2. gridstore : GridDB CE用のJavaクライアント。

    <dependency>
        <groupId>com.github.griddb</groupId>
        <artifactId>gridstore</artifactId>
        <version>5.1.0</version>
    </dependency>

GridDB 設定の定義

APIエンドポイントを作成する前に、アプリケーションとGridDBのクラスタデータベースとの接続を取得する必要があります。ここでは、GridStore クラスのインスタンスを作成します。

@Bean
public GridStore gridStore() throws GSException {
    // Acquiring a GridStore instance
    Properties properties = new Properties();
    properties.setProperty("notificationMember", "griddbserver:10001");
    properties.setProperty("clusterName", "dockerGridDB");
    properties.setProperty("user", "admin");
    properties.setProperty("password", "admin");
    GridStore store = GridStoreFactory.getInstance().getGridStore(properties);
    return store;
}

イベント・データ・モデルの実装

これは、イベント・データを保持するJavaクラスです。タイムスタンプ・カラムを主キーとして設定します。

import java.util.Date;
import com.toshiba.mwcloud.gs.RowKey;
import lombok.Data;

@Data
public class Event {
    @RowKey
    Date timestamp;
    String appId;
    String eventType;
    String externalId;
}
  • appId: Owner ID of the Mobile App or Website.
  • eventType: “ADD_CART”, “ADD_TO_WISHLIST”, “IN_APP_AD_CLICK”, “SEARCH”, “LOGIN”, “LAUNCH_APP”, “START_TRIAL”
  • externalId: External userId

行を管理するコンテナを作成する。

DB 接続を設定したら、それを使ってコンテナを作成します。ここでは、一連のイベントを格納するために TimeSeries コンテナを使用します。

import com.toshiba.mwcloud.gs.TimeSeries;

@Bean
public TimeSeries<event> eventsContainer(GridStore gridStore) throws GSException {
    return gridStore.putTimeSeries("events", Event.class);
}
  • com.toshiba.mwcloud.gs.TimeSeriesインタフェースは、TimeSeriesデータの操作に特化したコンテナです。

イベントの取得

イベントをキャプチャするための RESTful エンドポイントを作成する必要があります。このチュートリアルでは、1つのエンドポイントのみを提供します。

イベント(例: LaunchAPP, Purchase, Login, Add to Cart) を報告するには、/app/track/ エンドポイントに POST リクエストを送信します。

| Request address | http://localhost:8080/api/track |
| -------- | -------- |
| Request method     | **POST**     |

Request Example
curl --request POST --url http://localhost:8080/api/track \
 --header 'content-type: application/json'  --header 'user-agent: vscode-restclient' \
   --data '{
    "appId":"APP_1",
    "eventType":"ADD_CART",
    "externalId":"2385254254",
    "timestamp":"2023-07-12T10:55:50.764Z"
}'
Response Example
HTTPS/1.1 200 OK
Saved
  • このエンドポイントは、成功した場合は HTTP Status 200 を返し、エラーが発生した場合は例外を投げます。

GridDBのJavaクライアントライブラリを利用したイベント処理・保存ロジックの実装

イベントデータを保存するには、まず、先ほど作成した Container を取得し、 append メソッドを呼び出します。

public void create(EventRequest request) {
    Event event = new Event();
    event.setAppId(request.getAppId());
    event.setEventType(request.getEventType());
    event.setExternalId(request.getExternalId());
    event.setTimestamp(request.getTimestamp());
    try {
        eventsContainer.append(event);
    } catch (GSException e) {
        e.printStackTrace();
    }
}

レポーティング・ウェブ・インターフェース

さて、リアルタイムのダッシュボードを提供する必要がある。このチュートリアルでは、日付別とイベントタイプ別にイベントのカウントを表示する2つのチャートを作成します。

チャートの作成

Thymeleafテンプレートの上にチャートを表示するためにChartjsを使います。

Chart.jsはよく使われるチャートの種類、プラグイン、カスタマイズオプションを提供しています。合理的なビルトインチャートタイプのセットに加えて、コミュニティが管理する追加チャートタイプを使用することができます。その上、複数のチャート・タイプを組み合わせて混合チャート(基本的に、複数のチャート・タイプを同じキャンバス上で1つにブレンドすること)にすることも可能です。

単一のデータセットに対して棒グラフを作成し、HTMLページにレンダリングします。

<script th:inline="javascript">
    Chart.register(ChartDataLabels);
    const ctx = document.getElementById('charts');

    const data = /*[[${aggregates}]]*/[
        { timeLabel: "2023-07-01", count: 10 }
    ];

    new Chart(ctx, {
        type: 'bar',
        data: {
            labels: data.map(row => row.timeLabel),
            datasets: [{
                label: 'Count by Date',
                data: data.map(row => row.count),
                borderWidth: 1
            }]
        },
        options: {
            scales: {
                y: {
                    beginAtZero: true
                }
            },
            plugins: {
                datalabels: {
                    color: 'white',
                    font: {
                        weight: 'bold'
                    }
                }
            }
        }
    });

日付別イベント数の表示

イベントタイプ別のイベント数の表示

イベント集計

データセットをchartjsに提供するために、時系列行の集約処理を使用する必要があります。

public List getEvents() {
    List views = new ArrayList<>();

    LocalDateTime startDate = LocalDateTime.parse("2023-07-01T00:00:00"),
            endDate = LocalDateTime.now();
    for (LocalDateTime date = startDate; date.isBefore(endDate); date = date.plusDays(1)) {
        try {
            java.util.Date start =
                    convertToDateViaInstant(date.withHour(0).withMinute(0).withSecond(0));
            java.util.Date end =
                    convertToDateViaInstant(date.withHour(23).withMinute(59).withSecond(59));
            AggregationResult aggregationResult =
                    eventsContainer.aggregate(start, end, "appId", Aggregation.COUNT);
            Long count = aggregationResult.getLong();
            if (count.compareTo(0L) > 0) {
                views.add(new EventAggregateView(convertToDateViaInstant(date), count,
                        date.format(dateTimeFormatter)));
            }
        } catch (GSException e) {
            e.printStackTrace();
        }
    }
    return views;
}

TimeSeries コンテナの集約関数を使用します。

AggregationResult aggregate(java.util.Date start,
                          java.util.Date end,
                          java.lang.String column,
                          Aggregation aggregation)
                            throws GSException

指定された開始時刻と終了時刻に基づいて、行セットまたはその特定の列に対して集約処理を実行します。パラメータ列は、パラメータ集約によっては無視される場合があります。タイムスタンプが開始時刻または終了時刻と一致する境界行は、操作範囲に含まれます。指定された開始時刻が終了時刻よりも遅い場合、すべての行が操作範囲から除外されます。

イベント生成をシミュレートするために、1分ごとに起動するスケジュールタスクを作成します。

@Scheduled(fixedDelay = 1, initialDelay = 1, timeUnit = TimeUnit.MINUTES)
public void scheduleEventCreation() throws IOException, InterruptedException {
    log.info("scheduleEventCreation()");

    HttpClient client = HttpClient.newHttpClient();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    for (int i = 0; i <= 10; i++) {
        Runnable runnableTask = () -> {
            try {
                postEvent(client);
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        };
        executorService.schedule(runnableTask, i, TimeUnit.SECONDS);
    }
}

private void postEvent(HttpClient client)
        throws JsonProcessingException, IOException, InterruptedException {
    Instant instant = Instant.now();
    var values = new HashMap<String, String>() {
        {
            put("appId", "APP_1");
            put("eventType", getRandomElement(EventTrackerConstants.EVENT_TYPE_LIST));
            put("externalId", faker.idNumber().valid());
            put("timestamp", instant.toString());
        }
    };

    var objectMapper = new ObjectMapper();
    String requestBody = objectMapper.writeValueAsString(values);

    HttpRequest request =
            HttpRequest.newBuilder().uri(URI.create("http://localhost:8080/api/track"))
                    .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                    .header("Content-type", "application/json").build();

    client.send(request, HttpResponse.BodyHandlers.ofString());
}

Docker Composeを使ったプロジェクトの実行

プロジェクトを立ち上げるには、dockerとdocker-composeを利用します。必要なのはdockerをインストールすることだけです。そして以下のコマンドを実行します:

1. docker イメージをビルドします:

docker compose build

2. docker イメージを実行します:

docker compose up

すべてのイベントを見るには、http://localhost:8080/events を開いてください。

チャートを見るには http://localhost:8080/aggregate を開いてください。

ベストプラクティス

Spring Bootでリアルタイムイベント追跡を最大限に活用するには、いくつかのベストプラクティスに従うことが重要です。以下はリアルタイムイベント追跡の可能性を最大限に活用するための推奨事項です:

  • 関連するすべてのユーザーアクションとインタラクションをキャプチャする、明確で一貫性のあるイベントスキーマの定義に時間をかけましょう。また、ユーザーの状態や環境に関する洞察を提供する関連するコンテキスト情報をキャプチャすることも重要です。
  • システムがデータ保護規制に準拠していることを確認し、適切なセキュリティ対策を実施します。
  • ストリーム処理エンジンを使用して分析パイプラインを実装します。
  • イベント・ドリブン・アーキテクチャーとApache KafkaやRabbitMQのようなメッセージング・テクノロジーを組み合わせて、イベント・プロデューサーとコンシューマーをデカップリングします。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *