Cloudflare での WebSocket 通信のカウントを確認する

# 結論

:::note info
- リクエスト数
    - 1つの WebSocket 通信（Open から Close まで）を、1つの（長時間の）HTTP リクエストとしてカウント
- 帯域幅
    - Cloudflare から Client への「HTTP レスポンス + WebSocket メッセージデータ」の総量をカウント
:::

:::note info
ダッシュボードやログに表示されるタイミングは、WebSocket 通信 `Close` 後です。 
:::

2024/10/15 追記

:::note info
ドキュメントに追加されました

https://developers.cloudflare.com/network/websockets/#requests-and-bandwidth-measurement

>**Requests and Bandwidth measurement**
>
>Given the nature of WebSocket connections, you may notice they differ from typical HTTP traffic in terms of requests and bandwidth usage. If you are an Enterprise customer, it is important to consider how Cloudflare measures requests and bandwidth to accurately estimate your usage.
>
>Cloudflare measures a single WebSocket connection in the following way:
>
>- **Requests**: Cloudflare recognizes only the initial upgrade request per WebSocket connection as an HTTP request. Even though you can send a bidirectional message stream through the established WebSocket connection, it will be counted as a single long-lived HTTP request.
>
>- **Bandwidth**: Cloudflare measures data transfer sent from Cloudflare to the client. This typically means that messages from the WebSocket server behind Cloudflare to the WebSocket client are counted towards bandwidth usage.
>
>Once a WebSocket connection is closed, you can view your aggregated WebSocket usage through Traffic Analytics, the GraphQL Analytics API, and HTTP requests logs.
:::

# WebSocket 通信
以下のような通信フローとなります。

```mermaid:
%%{init:{'themeCSS':' g:nth-of-type(2) rect.actor { fill: orange; }; g:nth-of-type(5) rect.actor { fill: orange; };'}}%%
sequenceDiagram
    Client->>Cloudflare: HTTP Upgrade Request
    Cloudflare->>Server: HTTP Upgrade Request
    Server->>Cloudflare: HTTP "101 Switching Protocols" Response
    Cloudflare->>Client: HTTP "101 Switching Protocols" Response
    participant Cloudflare
    rect rgb(128,128,128)
    note right of Client: Open WebSocket connection
        Client->>Server: bidirectional message streams
        Server->>Client: bidirectional message streams
    end
    rect rgb(128,128,128)
        Client->>Server: bidirectional connection close event
        Server->>Client: bidirectional connection close event
    note right of Client: Close WebSocket connection
    end
```

# 検証セットアップ
以下の記事を参考にします。

`wscat` コマンドを使って WebSocket 接続を確立した上で、Server からメッセージを大量に送ります。

https://qiita.com/khayama/items/7332845c1ac0151f6f69

# ダッシュボード（GraphQL 統計）

1回の WebSocket 通信の中で Server 側から大量のメッセージデータを送ると以下のような表示となります。

リクエスト数としては、1つの WebSocket 通信（Open から Close まで）が、1つの（長時間の）HTTP リクエストとしてカウントされることがわかります。

帯域幅としては、Cloudflare から Client への「HTTP レスポンス + WebSocket メッセージデータ」の総量となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/db20bbff-039d-5a2d-9935-af331d34fb54.png)

# ログ
Cloudflare Logpush によって出力できる HTTP requests のログを確認します。

https://developers.cloudflare.com/logs/reference/log-fields/zone/http_requests/

1回の WebSocket 通信では、1件のログしか出てきません。

- `RequestHeaders` フィールドを設定の上で WebSocket 開始時のリクエストヘッダを確認できます。
    - `"RequestHeaders": { "upgrade": "websocket" }`
- HTTP レスポンス `101 Switching Protocols` を確認できます。
    - `"EdgeResponseStatus": 101,`
    - `"OriginResponseStatus": 101,`
- 帯域幅の総量「HTTP レスポンス + WebSocket メッセージデータ」は `EdgeResponseBytes` で確認でき、WebSocket メッセージデータ量単体は `EdgeResponseBodyBytes` に該当します。
    - `"EdgeResponseBytes": 19135192,`
    - `"EdgeResponseBodyBytes": 19133196,`
- WebSocket 通信時間は、`EdgeStartTimestamp` （2024年10月11日09:50:48）から`EdgeEndTimestamp`（2024年10月11日09:55:08）までと確認できます。
    - `"EdgeStartTimestamp": 1728607848471000000,` (Unix nano 秒)
    - `"EdgeEndTimestamp": 1728608108071000000,` (Unix nano 秒)

```
{
  ...
  "BotScore": 1,
  "BotScoreSrc": "Heuristics",
  "BotTags": [],
  "CacheCacheStatus": "dynamic",
  "CacheReserveUsed": false,
  "CacheResponseBytes": 19134836,
  "CacheResponseStatus": 101,
  "CacheTieredFill": false,
  ...
  "ClientRequestBytes": 3702,
  "ClientRequestHost": "wscat.example.com:8443",
  "ClientRequestMethod": "GET",
  "ClientRequestPath": "/",
  "ClientRequestProtocol": "HTTP/1.1",
  "ClientRequestReferer": "",
  "ClientRequestScheme": "https",
  "ClientRequestSource": "eyeball",
  "ClientRequestURI": "/",
  "ClientRequestUserAgent": "",
  "ClientSSLCipher": "AEAD-AES256-GCM-SHA384",
  "ClientSSLProtocol": "TLSv1.3",
  "ClientSrcPort": 56896,
  "ClientTCPRTTMs": 5,
  ...
  "EdgeColoCode": "NRT",
  "EdgeColoID": 408,
  "EdgeEndTimestamp": 1728608108071000000,
  "EdgePathingOp": "wl",
  "EdgePathingSrc": "macro",
  "EdgePathingStatus": "nr",
  "EdgeRateLimitAction": "",
  "EdgeRateLimitID": 0,
  "EdgeRequestHost": "wscat.example.com:8443",
  "EdgeResponseBodyBytes": 19133196,
  "EdgeResponseBytes": 19135192,
  "EdgeResponseCompressionRatio": 1,
  "EdgeResponseContentType": "",
  "EdgeResponseStatus": 101,
  "EdgeServerIP": "172.68.119.177",
  "EdgeStartTimestamp": 1728607848471000000,
  "EdgeTimeToFirstByteMs": 45,
  ...
  "OriginDNSResponseTimeMs": 2,
  "OriginIP": "x.x.x.x",
  "OriginRequestHeaderSendDurationMs": 0,
  "OriginResponseBytes": 0,
  "OriginResponseDurationMs": 16,
  "OriginResponseHTTPExpires": "",
  "OriginResponseHTTPLastModified": "",
  "OriginResponseHeaderReceiveDurationMs": 9,
  "OriginResponseStatus": 101,
  "OriginResponseTime": 16000000,
  "OriginSSLProtocol": "TLSv1.3",
  "OriginTCPHandshakeDurationMs": 3,
  "OriginTLSHandshakeDurationMs": 4,
  "ParentRayID": "00",
  "RayID": "8d0adbacfd353bfc",
  "RequestHeaders": {
    "connection": "Upgrade",
    "sec-websocket-key": "8QLHQtslEs/D33PgRGaHtw==",
    "sec-websocket-version": "13",
    "upgrade": "websocket"
  },
  ...
}
```

# WebSocket メッセージデータの量による比較

- WebSocket 接続後に Server 側から大量のメッセージデータを送った場合には、相応の `EdgeResponseBodyBytes` が確認できます。
- WebSocket 接続後にすぐに接続を切った場合には `EdgeResponseBodyBytes` が `0` であることが確認できます。

https://zenn.dev/oymk/articles/af803af11d4792

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/3665cb31-429f-0025-8fdd-3fc59b532547.png)

# まとめ
Cloudflare での WebSocket 通信のカウントを確認しました。

Cloudflare 契約時の利用量（リクエスト数・帯域幅）の見積もりにも同じ考え方が適用されるため、WebSocket 通信を利用するウェブサイトの場合はこの内容を念頭に検討が必要です。