Yuki Takei 11 часов назад
Родитель
Сommit
a0564b15d2

+ 90 - 0
.kiro/specs/opentelemetry/brief.md

@@ -0,0 +1,90 @@
+# Brief: opentelemetry
+
+## Problem
+GROWI は監視・可観測性のために OpenTelemetry を採用しており、`apps/app/src/features/opentelemetry/` 配下に NodeSDK 初期化・Resource Attribute・Custom Metrics・Anonymization の各レイヤを実装している。本 spec は `features/opentelemetry/` の **大局的なメンテナンスリファレンス** として、将来の追加・変更(メトリクス追加、新規 anonymization handler、SDK バージョンアップ)が踏むべき境界線と設計意図を提供する。
+
+## Current State
+- ランタイム: Node.js `^24`(cgroup 系 API・V8 統計が利用可能)。
+- 依存パッケージ:
+  - `@opentelemetry/api ^1.9.0`
+  - `@opentelemetry/sdk-node ^0.217.0`
+  - `@opentelemetry/auto-instrumentations-node ^0.75.0`
+  - `@opentelemetry/exporter-trace-otlp-grpc`, `@opentelemetry/exporter-metrics-otlp-grpc ^0.202.0`
+  - `@opentelemetry/sdk-metrics ^2.0.1`, `@opentelemetry/resources ^2.0.1`, `@opentelemetry/sdk-trace-node ^2.0.1`
+  - `@opentelemetry/semantic-conventions ^1.34.0`
+- 全コードは server-only。クライアント側からの import は無い。
+- ディレクトリ構成 (`apps/app/src/features/opentelemetry/server/`):
+  - `node-sdk.ts` — SDK ライフサイクル管理(`initInstrumentation` / `setupAdditionalResourceAttributes` / `startOpenTelemetry`)。
+  - `node-sdk-configuration.ts` — `NodeSDKConfiguration` 構築と Resource 構築(2 段階初期化)。
+  - `node-sdk-resource.ts` — `NodeSDK._resource` への低レベルアクセサ(リフレクション)。
+  - `logger.ts` — `DiagLogger` を pino logger にアダプトする実装。
+  - `semconv.ts` — incubating semantic conventions のコピー(`service.instance.id`, `http.target`)。
+  - `custom-resource-attributes/` — `os-resource-attributes` / `application-resource-attributes`。identity 専用。
+  - `custom-metrics/` — `application-metrics` / `user-counts-metrics` / `page-counts-metrics` / `system-metrics` + `setupCustomMetrics()` 合成。
+  - `anonymization/` — `httpInstrumentationConfig` と 4 個の handler(search / page-listing / page / page-access)。
+- 設定キー(`config-definition.ts`):
+  - `otel:enabled` (`OPENTELEMETRY_ENABLED`, default `true`)
+  - `otel:isAppSiteUrlHashed` (`OPENTELEMETRY_IS_APP_SITE_URL_HASHED`, default `false`)
+  - `otel:anonymizeInBestEffort` (`OPENTELEMETRY_ANONYMIZE_IN_BEST_EFFORT`, default `false`)
+  - `otel:serviceInstanceId` (`OPENTELEMETRY_SERVICE_INSTANCE_ID`, default `undefined`)
+- Exporter: OTLP gRPC(trace / metric とも)。Endpoint は OTel 標準環境変数(`OTEL_EXPORTER_OTLP_ENDPOINT` 等)で制御。
+- Metric 出力間隔: `PeriodicExportingMetricReader` の `exportIntervalMillis: 300000`(5 分)。
+
+## Desired Outcome
+- `features/opentelemetry/` のすべての公開モジュールが本 spec の Boundary Commitments / Out of Boundary で明示的に分類されており、新規メトリクス追加・新規 anonymization handler 追加・SDK バージョンアップが「どこを触ればよいか / どこを触ってはいけないか」を本 spec 1 か所で参照できる。
+- Resource Attribute は identity 専用、設定値は `growi.configs` info-gauge ラベルへ、観測値は `growi.*` または `system.*` / `process.*` メトリクスへ、というレイヤ責務が明文化されている。
+- 旧来の `apps/app/src/features/opentelemetry/docs/` 配下の散在ドキュメントは破棄され、本 spec が単一の真実ソースになる。
+
+## Approach
+**新規実装ではなくドキュメント統合。** 既に動作している `features/opentelemetry/` の構造を本 spec に固定化する。
+1. SDK ライフサイクル・Resource 2 段階初期化・Custom Metrics 合成・Anonymization の 4 レイヤを Boundary Commitments で分割。
+2. Configuration(env var / config key)と Metric Schema を表形式で明示。
+3. Resource Attribute は identity 専用、設定値は `growi.configs` info-gauge ラベル、観測値(メモリ・ヒープ等)は `system.*` / `process.*` メトリクスへ、というレイヤ責務を Design Decisions として固定する。
+
+## Scope
+- **In**:
+  - `features/opentelemetry/server/` 配下のすべての公開モジュール(SDK / Resource / Metric / Anonymization / Logger / semconv)の責務と境界の明文化。
+  - 設定キー一覧と Resource Attribute / Metric Schema の確定スナップショット。
+  - 既存 Anonymization Handler の登録手順(`handlers/index.ts` への module 追加 + `canHandle` / `handle` インターフェース実装)。
+- **Out**:
+  - 既存メトリクスの名称変更や再構成。
+  - Trace span attribute の追加(`http.target` 以外)。
+  - GROWI 本体の logger pipeline と OpenTelemetry log signal の統合。
+  - フロントエンド(ブラウザ)からの telemetry 出力。
+  - サードパーティ製パッケージ(`@opentelemetry/host-metrics` 等)への置き換え。
+
+## Boundary Candidates
+1. **SDK ライフサイクル**(`node-sdk.ts`, `node-sdk-configuration.ts`, `node-sdk-resource.ts`, `logger.ts`) — SDK 初期化・enable/disable 制御・Resource 2 段階注入。
+2. **Resource Attribute レイヤ**(`custom-resource-attributes/`) — identity 専用属性の供給。
+3. **Custom Metric レイヤ**(`custom-metrics/`) — `growi.*` / `system.*` / `process.*` メトリクスの emit と合成。
+4. **HTTP Anonymization レイヤ**(`anonymization/`) — `http.target` の匿名化と handler の選択ロジック。
+5. **SemConv ローカルコピー**(`semconv.ts`) — 不安定 semconv の固定化。
+
+これら 5 つはそれぞれ独立に拡張・置換可能で、相互の dependency は明確に下流方向に限定されている。
+
+## Out of Boundary
+- `~/server/service/growi-info`(`growiInfoService`) — 上流。本 spec は consumer。
+- `~/server/service/config-manager`(`configManager`) — 上流。本 spec は consumer。
+- `~/utils/growi-version` / `~/utils/logger` — utility。本 spec は consumer。
+- Anonymization で参照する `@growi/core/dist/utils/page-path-utils` の各 helper(`isPermalink`, `isUserPage`, `getUsernameByPath` 等) — `@growi/core` の責務。
+- Auto-instrumentation(HTTP / Express / Mongoose 等)のチューニング — 設定オブジェクトの構造は本 spec が定義するが、各 instrumentation の挙動は上流パッケージの責務。
+- OTLP Exporter の wire 仕様、Prometheus / Grafana / Collector 等の下流ツールチェイン。
+
+## Upstream / Downstream
+- **Upstream**:
+  - `~/server/service/config-manager` — `otel:*` config 4 種。
+  - `~/server/service/growi-info` — `growiInfoService.getGrowiInfo(opts)`。Metric / Resource 双方が consumer。
+  - `~/utils/growi-version` — `service.version` Resource Attribute の供給元。
+- **Downstream**:
+  - OpenTelemetry Collector(OTLP gRPC)。
+  - 受信側ダッシュボード(Prometheus / Grafana / Tempo / Loki 等)。otel-infra 管理者は本 spec の Metric Schema と Resource Attribute 表を参照する。
+
+## Existing Spec Touchpoints
+- **Adjacent**: なし。`growi-logger` spec はアプリケーションロガーの spec で、`logger.ts` の `DiagLogger` アダプタが pino を経由する点で接点があるが、両者の責務は独立。
+
+## Constraints
+- ランタイム要件: Node.js `^24`(cgroup memory API、V8 統計のため)。
+- 新規 npm dependency の追加は原則不可(既存 `@opentelemetry/*` パッケージで完結させる)。追加が必要な場合は `apps/app/.next/node_modules/` 残留有無を確認し `dependencies` 分類が必要かを判定する(参照: `.claude/rules/package-dependencies.md`)。
+- Semconv の不安定 attribute は `semconv.ts` にローカルコピーする(incubating entry-point は import しない)。詳細は [semconv.ts](../../apps/app/src/features/opentelemetry/server/semconv.ts) のコメント参照。
+- `setResource()` は `NodeSDK._resource` への private アクセスを行う(type cast 必須)。OpenTelemetry SDK が public な resource 上書き API を提供したら撤去する。
+- Anonymization の出力先 attribute は `http.target`(incubating)。OTLP semconv で対応 stable attribute が決定したら移行する。

+ 737 - 0
.kiro/specs/opentelemetry/design.md

@@ -0,0 +1,737 @@
+# Technical Design — opentelemetry
+
+## Overview
+
+**Purpose**: GROWI の OpenTelemetry 統合 (`apps/app/src/features/opentelemetry/`) を、SDK ライフサイクル / Resource Attribute / Custom Metric / HTTP Anonymization の 4 レイヤに分けて責務境界を明文化する大局的なメンテナンス spec。
+
+**Users**:
+- GROWI 開発者(メトリクス追加・anonymization handler 追加・SDK バージョンアップを行う)。
+- OpenTelemetry 受信側インフラ管理者(Prometheus / Grafana / Collector を運用する)。
+
+**Impact**: 既に稼働している実装の現状をスナップショットとして固定化する。新規実装はゼロで、コード変更は伴わない。将来の機能追加・変更は本 spec の Boundary Commitments に従って境界の中で行われる。
+
+### Goals
+
+- 4 レイヤそれぞれの責務・境界・依存関係を明文化する。
+- Resource Attribute は identity 専用、設定値は `growi.configs` ラベル、観測値は `growi.*` / `system.*` / `process.*` メトリクスへ、というレイヤ分離を維持する。
+- 新規 Custom Metric / Anonymization Handler の追加手順を「テンプレート化」して、追加時のレビュー差分を最小化する。
+- Resource Attribute / Metric / Span Attribute の責務分離(identity / 設定値 / 観測値 / span attribute)を Design Decisions として固定する。
+
+### Non-Goals
+
+- 既存メトリクス / Resource Attribute の名称変更・再構成。
+- OpenTelemetry Log Signal の利用開始。
+- ブラウザサイドからの telemetry 出力。
+- `@opentelemetry/host-metrics` 等への置き換え。
+- `service.instance.id` の自動生成(現状は `otel:serviceInstanceId` か `app:serviceInstanceId` の config 値を passthrough する)。
+
+## Boundary Commitments
+
+### This Spec Owns
+
+#### Layer 1: SDK ライフサイクル
+
+- `node-sdk.ts` の `initInstrumentation()` / `setupAdditionalResourceAttributes()` / `startOpenTelemetry()` の 3 関数。
+- `overwriteSdkDisabled()` による `OTEL_SDK_DISABLED` と `otel:enabled` の整合化。
+- `node-sdk-configuration.ts` の `generateNodeSDKConfiguration(opts)` および `generateAdditionalResourceAttributes(opts)`。
+- `node-sdk-resource.ts` の `getResource()` / `setResource()`(NodeSDK private `_resource` への reflective アクセサ)。
+- `logger.ts` の `DiagLoggerPinoAdapter` と `initLogger()`。
+- 同一プロセス内の SDK インスタンス二重生成防止(`sdkInstance` モジュール変数)。
+
+#### Layer 2: Resource Attribute(identity 専用)
+
+- `custom-resource-attributes/os-resource-attributes.ts` の `getOsResourceAttributes(): Attributes`。
+- `custom-resource-attributes/application-resource-attributes.ts` の `getApplicationResourceAttributes(): Promise<Attributes>`。
+- `custom-resource-attributes/index.ts` のバレル。
+- emit する identity 属性: `os.type`, `os.platform`, `os.arch`, `growi.service.type`, `growi.deployment.type`。
+- 2 段階初期化(DB 非依存の OS info → DB 初期化後の `service.instance.id` および application info)の責務。
+
+#### Layer 3: Custom Metric
+
+- `custom-metrics/index.ts` の barrel と `setupCustomMetrics(): Promise<void>`。
+- `custom-metrics/application-metrics.ts` の `addApplicationMetrics()`(`growi.configs` info gauge + 5 ラベル)。
+- `custom-metrics/user-counts-metrics.ts` の `addUserCountsMetrics()`(`growi.users.total` / `growi.users.active`)。
+- `custom-metrics/page-counts-metrics.ts` の `addPageCountsMetrics()`(`growi.pages.total`)。
+- `custom-metrics/system-metrics.ts` の `addSystemMetrics()`(`system.memory.limit` / `system.host.memory.total` / `process.memory.usage` / `process.runtime.v8.heap.{used,total,external}`)。
+- Meter 命名規約: `growi-<scope>-metrics`, version `'1.0.0'`。
+- Observation 実装規約: `ObservableGauge` + `addBatchObservableCallback` + try/catch + `diag.createComponentLogger` で例外吸収。
+
+#### Layer 4: HTTP Anonymization
+
+- `anonymization/index.ts` のバレル(`httpInstrumentationConfig` のエクスポート)。
+- `anonymization/anonymize-http-requests.ts` の `startIncomingSpanHook` 実装(module discovery loop)。
+- `anonymization/interfaces/anonymization-module.ts` の `AnonymizationModule` インターフェース。
+- `anonymization/handlers/index.ts` の `anonymizationModules` 配列(4 module の登録順)。
+- 4 つの handler 実装: `search-api-handler.ts`, `page-listing-api-handler.ts`, `page-api-handler.ts`, `page-access-handler.ts`。
+- `anonymization/utils/anonymize-query-params.ts` の `anonymizeQueryParams(target, paramNames)`。
+- `http.target` への匿名化 URL 出力。
+
+#### Layer 5: SemConv ローカルコピー
+
+- `semconv.ts` の `ATTR_SERVICE_INSTANCE_ID` / `ATTR_HTTP_TARGET` 定義。
+- incubating semconv の文字列定数化と上流 minor リリース変更からの隔離。
+
+### Out of Boundary
+
+- `~/server/service/growi-info`(`growiInfoService`) — consumer として利用するのみ。`getGrowiInfo(opts)` の API 仕様は本 spec では定義しない。
+- `~/server/service/config-manager` — `otel:*` config 4 種を読み取るのみ。config 定義の追加・改名は config-manager 側の責務。
+- `~/utils/growi-version` — `service.version` の供給元。
+- `~/utils/logger` — pino logger ファクトリ。
+- `@growi/core/dist/utils/page-path-utils` — anonymization で利用する path helper(`isPermalink` / `isUserPage` / `getUsernameByPath` 等)。
+- 各 `@opentelemetry/*` パッケージの内部実装。Auto-instrumentation の挙動チューニング(HTTP / Express / Mongoose 等の挙動は instrumentation の責務)。
+- Trace span への独自 attribute 付与(`http.target` 以外)。
+- OpenTelemetry Log Signal、ブラウザ telemetry。
+- OTLP wire 仕様および受信側ダッシュボード/アラート。
+
+### Allowed Dependencies
+
+- Node.js 標準モジュール: `node:os`, `node:v8`, `node:process`, `node:crypto`, `node:http`。
+- `@opentelemetry/api`: `metrics`, `diag`, `Attributes`, `Meter`, `ObservableGauge` 等の public API のみ。
+- `@opentelemetry/sdk-node`, `@opentelemetry/sdk-metrics`, `@opentelemetry/sdk-trace-node`, `@opentelemetry/resources`: SDK 初期化用。
+- `@opentelemetry/exporter-trace-otlp-grpc`, `@opentelemetry/exporter-metrics-otlp-grpc`: OTLP gRPC エクスポート。
+- `@opentelemetry/auto-instrumentations-node`: `getNodeAutoInstrumentations()` および `InstrumentationConfigMap` 型。
+- `@opentelemetry/semantic-conventions`: stable attribute のみ(`ATTR_SERVICE_NAME` / `ATTR_SERVICE_VERSION`)。incubating は import しない。
+- 上記以外の新規 npm dependency 追加は不可。追加する場合は `apps/app/.next/node_modules/` 残留有無の確認と `dependencies` 分類が必要(`.claude/rules/package-dependencies.md` 参照)。
+
+### Revalidation Triggers
+
+- `@opentelemetry/api` または `@opentelemetry/sdk-*` のメジャー更新 → Meter / ObservableGauge / NodeSDK API シグネチャの再確認、特に `node-sdk-resource.ts` の `_resource` private アクセスが public 化されていないか確認。
+- `growiInfoService.getGrowiInfo()` の API 変更(追加フラグ削除、返り値型変更)→ 該当する `application-metrics.ts` / `user-counts-metrics.ts` / `page-counts-metrics.ts` / `application-resource-attributes.ts` の参照を再確認。
+- `@opentelemetry/semantic-conventions` の `service.instance.id` / `http.target` の stable 化 → `semconv.ts` のローカル定数を撤去し、stable import に切り替える。
+- Node.js ランタイム要件のダウングレード(`engines.node` が `^24` 未満)→ `process.constrainedMemory()` / `v8.getHeapStatistics()` の互換性再確認。
+- 新規 anonymization 対象パス/パラメータ追加要望 → handler の登録順と canHandle 衝突の再評価。
+- 受信側ダッシュボード/クエリの参照更新が未完了の状態でメトリクス/ラベル変更を行う → ロールアウト順序の再調整。
+
+## Architecture
+
+### Existing Architecture Analysis
+
+`features/opentelemetry/server/` は以下の 5 レイヤを下流方向の単方向依存で構成する:
+
+1. **SDK ライフサイクル** — `node-sdk.ts` が `node-sdk-configuration.ts` / `node-sdk-resource.ts` / `logger.ts` および `custom-metrics/index.ts` を統括する。
+2. **Resource Attribute** — `custom-resource-attributes/` を `node-sdk-configuration.ts` の 2 段階目(`generateAdditionalResourceAttributes`)が consumer として呼ぶ。
+3. **Custom Metric** — `custom-metrics/index.ts` の `setupCustomMetrics()` が起動時に 4 モジュールを順次登録。各モジュールは `growiInfoService` または Node.js stdlib を参照。
+4. **HTTP Anonymization** — `anonymization/index.ts` から export される `httpInstrumentationConfig` が `node-sdk-configuration.ts` の auto-instrumentation 構築時に注入される。
+5. **SemConv** — `semconv.ts` は Layer 1, 2, 4 から参照される葉ノード。
+
+各レイヤは独立に拡張可能で、横断的な相互依存(Custom Metric が Anonymization に依存する等)は存在しない。
+
+### Architecture Pattern & Boundary Map
+
+```mermaid
+graph TB
+    subgraph Layer1["Layer 1: SDK lifecycle"]
+        NodeSdk[node-sdk.ts]
+        NodeSdkCfg[node-sdk-configuration.ts]
+        NodeSdkRes[node-sdk-resource.ts]
+        DiagLogger[logger.ts]
+    end
+    subgraph Layer2["Layer 2: Resource Attribute identity-only"]
+        OsRA[custom-resource-attributes/os-resource-attributes.ts]
+        AppRA[custom-resource-attributes/application-resource-attributes.ts]
+    end
+    subgraph Layer3["Layer 3: Custom Metric"]
+        MetIndex[custom-metrics/index.ts setupCustomMetrics]
+        AppMet[application-metrics.ts]
+        UserMet[user-counts-metrics.ts]
+        PageMet[page-counts-metrics.ts]
+        SysMet[system-metrics.ts]
+    end
+    subgraph Layer4["Layer 4: HTTP Anonymization"]
+        AnonIdx[anonymization/index.ts]
+        AnonHook[anonymize-http-requests.ts]
+        AnonHandlers[handlers/index.ts]
+        SearchH[search-api-handler.ts]
+        PageH[page-api-handler.ts]
+        PageListH[page-listing-api-handler.ts]
+        PageAccH[page-access-handler.ts]
+        QPUtil[utils/anonymize-query-params.ts]
+    end
+    SemConv[semconv.ts]
+    GrowiInfo[~/server/service/growi-info]
+    ConfigMgr[~/server/service/config-manager]
+    Logger[~/utils/logger]
+    Version[~/utils/growi-version]
+
+    NodeSdk --> NodeSdkCfg
+    NodeSdk --> NodeSdkRes
+    NodeSdk --> DiagLogger
+    NodeSdk --> MetIndex
+    NodeSdkCfg --> OsRA
+    NodeSdkCfg --> AppRA
+    NodeSdkCfg --> AnonIdx
+    NodeSdkCfg --> Version
+    NodeSdkCfg --> SemConv
+    AppRA --> GrowiInfo
+    AppMet --> GrowiInfo
+    AppMet --> ConfigMgr
+    UserMet --> GrowiInfo
+    PageMet --> GrowiInfo
+    AnonIdx --> AnonHook
+    AnonHook --> AnonHandlers
+    AnonHandlers --> SearchH
+    AnonHandlers --> PageH
+    AnonHandlers --> PageListH
+    AnonHandlers --> PageAccH
+    SearchH --> QPUtil
+    PageH --> QPUtil
+    PageListH --> QPUtil
+    SearchH --> SemConv
+    PageH --> SemConv
+    PageListH --> SemConv
+    PageAccH --> SemConv
+```
+
+### Bootstrap Sequence
+
+```mermaid
+sequenceDiagram
+    participant App as GROWI app (server entrypoint)
+    participant Lifecycle as Layer 1: node-sdk
+    participant Config as node-sdk-configuration
+    participant SDK as @opentelemetry/sdk-node
+    participant Res as Layer 2: Resource Attr
+    participant Met as Layer 3: setupCustomMetrics
+
+    App->>Lifecycle: initInstrumentation()
+    Lifecycle->>Lifecycle: configManager.loadConfigs(env)
+    Lifecycle->>Lifecycle: overwriteSdkDisabled()
+    alt otel:enabled === true
+        Lifecycle->>Config: generateNodeSDKConfiguration({ enableAnonymization })
+        Config->>Config: build Resource(service.name, service.version)
+        Config->>Config: getNodeAutoInstrumentations(httpInstrumentationConfig?)
+        Config-->>Lifecycle: Configuration
+        Lifecycle->>SDK: new NodeSDK(config)
+    end
+    Note over App: ... DB initialization completes ...
+    App->>Lifecycle: setupAdditionalResourceAttributes()
+    Lifecycle->>Config: generateAdditionalResourceAttributes()
+    Config->>Res: getOsResourceAttributes()
+    Config->>Res: getApplicationResourceAttributes()
+    Config->>Config: resource.merge(service.instance.id + osAttrs + appAttrs)
+    Config-->>Lifecycle: updatedResource
+    Lifecycle->>SDK: setResource(sdkInstance, updatedResource)
+    App->>Lifecycle: startOpenTelemetry()
+    Lifecycle->>SDK: sdkInstance.start()
+    Lifecycle->>Met: setupCustomMetrics()
+    Met->>Met: addApplicationMetrics()
+    Met->>Met: addUserCountsMetrics()
+    Met->>Met: addPageCountsMetrics()
+    Met->>Met: addSystemMetrics()
+```
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Runtime | Node.js `^24` | `process.constrainedMemory()` / `v8.getHeapStatistics()` 等 stdlib API | `apps/app/package.json` の `engines` ではなくリポジトリルートの `engines` で指定 |
+| Telemetry SDK | `@opentelemetry/api ^1.9.0`, `@opentelemetry/sdk-node ^0.217.0`, `@opentelemetry/sdk-metrics ^2.0.1`, `@opentelemetry/sdk-trace-node ^2.0.1`, `@opentelemetry/resources ^2.0.1` | NodeSDK / Meter / Resource | 既存導入済み |
+| Exporter | `@opentelemetry/exporter-trace-otlp-grpc`, `@opentelemetry/exporter-metrics-otlp-grpc ^0.202.0` | OTLP gRPC エクスポート | 引数なしで生成し endpoint は OTel 標準 env var で解決 |
+| Auto-instrumentation | `@opentelemetry/auto-instrumentations-node ^0.75.0` | HTTP / Express / Mongoose 等の自動計測 | `instrumentation-pino`, `instrumentation-fs` は無効化 |
+| SemConv | `@opentelemetry/semantic-conventions ^1.34.0` | stable attribute のみ import | incubating は `semconv.ts` にローカルコピー |
+| Logger | pino(`~/utils/logger`) + `diag` アダプタ | dev 環境のみ DiagLogger を pino に差し替え | production は OpenTelemetry の default diag |
+| Test | Vitest + `vitest-mock-extended` | `vi.mock('node:os'/'node:v8')`, `mock<Meter>()` パターン | 既存テスト基盤 |
+
+## File Structure Plan
+
+### Directory Structure
+
+```
+apps/app/src/features/opentelemetry/server/
+├── index.ts                              # public export: `export * from './node-sdk'`
+├── node-sdk.ts                           # Layer 1: SDK lifecycle entrypoints
+├── node-sdk-configuration.ts             # Layer 1: NodeSDKConfiguration + Resource builders
+├── node-sdk-resource.ts                  # Layer 1: NodeSDK._resource reflective accessor
+├── logger.ts                             # Layer 1: DiagLoggerPinoAdapter
+├── semconv.ts                            # Layer 5: incubating attribute local copy
+├── custom-resource-attributes/
+│   ├── index.ts                          # Layer 2: barrel
+│   ├── os-resource-attributes.ts         # Layer 2: OS identity (stage-1)
+│   ├── os-resource-attributes.spec.ts
+│   ├── application-resource-attributes.ts # Layer 2: GROWI service identity (stage-2)
+│   └── application-resource-attributes.spec.ts
+├── custom-metrics/
+│   ├── index.ts                          # Layer 3: barrel + setupCustomMetrics()
+│   ├── application-metrics.ts            # Layer 3: growi.configs info gauge
+│   ├── application-metrics.spec.ts
+│   ├── user-counts-metrics.ts            # Layer 3: growi.users.{total,active}
+│   ├── user-counts-metrics.spec.ts
+│   ├── page-counts-metrics.ts            # Layer 3: growi.pages.total
+│   ├── page-counts-metrics.spec.ts
+│   ├── system-metrics.ts                 # Layer 3: system.* / process.* memory metrics
+│   └── system-metrics.spec.ts
+└── anonymization/
+    ├── index.ts                          # Layer 4: barrel
+    ├── anonymize-http-requests.ts        # Layer 4: startIncomingSpanHook + module loop
+    ├── interfaces/
+    │   └── anonymization-module.ts       # Layer 4: AnonymizationModule interface
+    ├── handlers/
+    │   ├── index.ts                      # Layer 4: anonymizationModules[] registration
+    │   ├── search-api-handler.ts
+    │   ├── search-api-handler.spec.ts
+    │   ├── page-listing-api-handler.ts
+    │   ├── page-listing-api-handler.spec.ts
+    │   ├── page-api-handler.ts
+    │   ├── page-api-handler.spec.ts
+    │   ├── page-access-handler.ts
+    │   └── page-access-handler.spec.ts
+    └── utils/
+        ├── anonymize-query-params.ts
+        └── anonymize-query-params.spec.ts
+```
+
+### Extension Templates
+
+#### 新規 Custom Metric モジュールの追加
+
+1. `custom-metrics/<scope>-metrics.ts` を新規作成し、`addXxxMetrics(): void` を export する。
+2. ファイル冒頭で `loggerFactory('growi:opentelemetry:custom-metrics:<scope>')` と `diag.createComponentLogger({ namespace: 'growi:custom-metrics:<scope>' })` を初期化する。
+3. `metrics.getMeter('growi-<scope>-metrics', '1.0.0')` で Meter 取得。
+4. `meter.createObservableGauge(name, { description, unit })` で gauge 群を作成。
+5. `meter.addBatchObservableCallback(async (result) => { try { ... } catch (e) { loggerDiag.error(...) } }, [...gauges])` を 1 つ登録。
+6. `custom-metrics/index.ts` の barrel に `export { addXxxMetrics } from './<scope>-metrics';` を追加し、`setupCustomMetrics()` 内で dynamic import + 呼び出し。
+7. `*.spec.ts` を co-locate し、`vi.mock('@opentelemetry/api')` + `mock<Meter>()` パターンで unit test を書く。
+
+#### 新規 Anonymization Handler の追加
+
+1. `anonymization/handlers/<scope>-handler.ts` を新規作成し、`AnonymizationModule` 型の object を export する。
+2. `canHandle(url): boolean` で対象 URL を判別する。先頭一致 / `URL` parser / 正規表現を適宜使用。
+3. `handle(request, url): Record<string, string> | null` で `anonymizeQueryParams()` または独自ロジックを適用し、`{ [ATTR_HTTP_TARGET]: anonymizedUrl }` を返す。何も匿名化しない場合は `null`。
+4. `handlers/index.ts` の `anonymizationModules` 配列に追加。**順序が重要**: より具体的なパスを先に置く(API > 静的 page access)。
+5. `*.spec.ts` を co-locate し、`canHandle` の境界条件と `handle` の URL 変換を網羅する。
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Interfaces |
+|-------------|---------|------------|------------|
+| 1.1–1.4 | SDK ライフサイクルと有効化制御 | NodeSdkLifecycle, OverwriteSdkDisabled | `initInstrumentation()`, `setupAdditionalResourceAttributes()`, `startOpenTelemetry()` |
+| 2.1–2.4 | identity 専用 Resource Attribute | OsResourceAttributes, ApplicationResourceAttributes, NodeSdkConfiguration | `getOsResourceAttributes()`, `getApplicationResourceAttributes()`, `generateAdditionalResourceAttributes()` |
+| 3.1–3.5 | GROWI 設定情報の info-gauge ラベル統合 | ApplicationMetrics | `addApplicationMetrics()` の observe ラベル |
+| 4.1–4.4 | 業務カウントメトリクス | UserCountsMetrics, PageCountsMetrics | `addUserCountsMetrics()`, `addPageCountsMetrics()` |
+| 5.1–5.5 | コンテナ運用に対応したメモリ系メトリクス | SystemMetrics | `addSystemMetrics()` |
+| 6.1–6.5 | HTTP リクエストの best-effort anonymization | HttpInstrumentationConfig, AnonymizationModules | `httpInstrumentationConfig.startIncomingSpanHook`, 各 `AnonymizationModule.{canHandle,handle}` |
+| 7.1–7.3 | Diag Logger と pino の統合 | DiagLoggerPinoAdapter | `initLogger()` |
+| 8.1–8.3 | メトリクスエクスポートと SDK 設定 | NodeSdkConfiguration | `generateNodeSDKConfiguration()` の reader / exporter / instrumentation 設定 |
+| 9.1–9.2 | SemConv の不安定 attribute のローカルコピー | SemConv | `semconv.ts` の文字列定数 |
+| 10.1–10.3 | 拡張・追加時の境界遵守 | CustomMetricsIndex, AnonymizationHandlersIndex | 拡張テンプレート(File Structure Plan 参照) |
+
+## Components and Interfaces
+
+### Layer 1: SDK ライフサイクル
+
+#### NodeSdkLifecycle
+
+| Field | Detail |
+|-------|--------|
+| Intent | OpenTelemetry SDK のプロセス内ライフサイクル管理 |
+| Requirements | 1.1, 1.2, 1.3, 1.4, 8.1, 8.2, 8.3 |
+
+**Responsibilities & Constraints**
+- 同一プロセス内で 1 つの `NodeSDK` インスタンスのみを保持する。
+- `otel:enabled` が `false` のときは SDK を構築しない。
+- `OTEL_SDK_DISABLED` env var と `otel:enabled` の食い違いを warn で報告し上書きする。
+- Resource 注入は 2 段階(SDK 構築時 / DB 初期化後)に分け、`setResource()` の private API 経由で 2 段階目を反映する。
+- `start()` 直後に `setupCustomMetrics()` を呼び出して Custom Metric の登録を行う。
+
+**Dependencies**
+- Inbound: `apps/app/src/server/app.ts` 系の起動シーケンス(実体は本 spec の外)。
+- Outbound: `@opentelemetry/sdk-node` (`NodeSDK`), `configManager`, `./node-sdk-configuration`, `./node-sdk-resource`, `./logger`, `./custom-metrics`.
+
+##### Service Interface
+```typescript
+export const initInstrumentation: () => Promise<void>;
+export const setupAdditionalResourceAttributes: () => Promise<void>;
+export const startOpenTelemetry: () => void;
+// テスト専用
+export const __testing__: { getSdkInstance, reset };
+```
+
+**Implementation Notes**
+- 二重 init 防止: モジュールスコープの `let sdkInstance: NodeSDK | undefined;` を見て、設定済みなら warn のみで return。
+- `start()` 前後で `instrumentationEnabled` を再確認する(dev 時に env を切り替える運用への配慮)。
+- `setResource()` は `NodeSDK._resource` を直接書き換える。OpenTelemetry SDK が public な resource 上書き API を提供したら撤去する候補(Revalidation Trigger)。
+
+#### NodeSdkConfiguration
+
+| Field | Detail |
+|-------|--------|
+| Intent | `NodeSDKConfiguration` オブジェクトと Resource の構築 |
+| Requirements | 2.1, 2.2, 2.3, 6.1, 6.5, 8.1, 8.2, 8.3 |
+
+**Responsibilities & Constraints**
+- 1 段階目 Resource: `{ service.name: 'growi', service.version: <growi-version> }`。
+- 2 段階目 Resource: `{ service.instance.id?, ...osAttrs, ...appAttrs }` を merge。
+- Trace exporter: `OTLPTraceExporter()`(引数なし)。
+- Metric reader: `PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter(), exportIntervalMillis: 300000 })`。
+- Instrumentation: `getNodeAutoInstrumentations({ pino: disabled, fs: disabled, http: { enabled, ...httpInstrumentationConfig } })`。
+- `enableAnonymization` が `true` のときのみ `httpInstrumentationConfig` を注入。
+
+**Dependencies**
+- Outbound: `@opentelemetry/sdk-node`, `@opentelemetry/sdk-metrics`, `@opentelemetry/exporter-*-otlp-grpc`, `@opentelemetry/auto-instrumentations-node`, `@opentelemetry/resources`, `@opentelemetry/semantic-conventions` (stable), `./semconv`, `./anonymization`, `./custom-resource-attributes`, `~/server/service/config-manager`, `~/utils/growi-version`.
+
+##### Service Interface
+```typescript
+type Option = { enableAnonymization?: boolean };
+type Configuration = Partial<NodeSDKConfiguration> & { resource: Resource };
+export const generateNodeSDKConfiguration: (opts?: Option) => Configuration;
+export const generateAdditionalResourceAttributes: (opts?: Option) => Promise<Resource>;
+```
+
+**Implementation Notes**
+- `configuration` と `resource` をモジュールスコープで保持し、二重生成を防止する。
+- `service.instance.id` の値は `otel:serviceInstanceId` を優先し、フォールバックで `app:serviceInstanceId`。
+
+#### NodeSdkResource
+
+| Field | Detail |
+|-------|--------|
+| Intent | NodeSDK の private `_resource` プロパティへの reflective アクセス |
+| Requirements | 1.3 |
+
+**Responsibilities & Constraints**
+- `getResource(sdk)`: `_resource` の存在を検証し返す。失敗時は throw。
+- `setResource(sdk, resource)`: `getResource` で生存確認した上で `_resource` を上書き。
+
+**Implementation Notes**
+- `as any` キャストでアクセスする。SDK のメジャー更新時に public API が出たら即座に撤去すべき箇所(Revalidation Trigger)。
+
+#### DiagLoggerPinoAdapter
+
+| Field | Detail |
+|-------|--------|
+| Intent | `@opentelemetry/api` の `DiagLogger` を pino logger にアダプトする |
+| Requirements | 7.1, 7.2, 7.3 |
+
+**Responsibilities & Constraints**
+- 開発環境(`NODE_ENV === 'development'`)でのみ `initLogger()` を `node-sdk.ts` から呼び出す。
+- `parseMessage(message, args)` で JSON 文字列を構造化 data に変換し、`logger.error(data, msg)` の pino 引数規約に整合する。
+- `error` / `warn` / `info` / `debug` / `verbose` の 5 メソッドを実装。`verbose` は pino の `trace` レベルにマップ。
+
+### Layer 2: Resource Attribute
+
+#### OsResourceAttributes
+
+| Field | Detail |
+|-------|--------|
+| Intent | OS identity を OTel Resource Attribute として返す |
+| Requirements | 2.1, 2.3 |
+
+**Responsibilities & Constraints**
+- `os.type` / `os.platform` / `os.arch` を返す。
+- 測定値(`os.totalmem` 等)は返さない(System Metric 側の責務)。
+
+**Dependencies**
+- External: `node:os` (`type()`, `platform()`, `arch()`).
+
+##### Service Interface
+```typescript
+export function getOsResourceAttributes(): Attributes;
+// 戻り値: { 'os.type': string, 'os.platform': string, 'os.arch': string }
+```
+
+#### ApplicationResourceAttributes
+
+| Field | Detail |
+|-------|--------|
+| Intent | GROWI service identity を OTel Resource Attribute として返す |
+| Requirements | 2.1, 2.3, 2.4 |
+
+**Responsibilities & Constraints**
+- `growi.service.type` / `growi.deployment.type` を返す。
+- サブシステム設定値(`growi.attachment.type` 等)は返さない(`growi.configs` ラベル側の責務)。
+- try/catch で `growiInfoService` の失敗を吸収し、空 attributes を返す。
+
+**Dependencies**
+- Outbound: `~/server/service/growi-info` の `growiInfoService.getGrowiInfo({})`(dynamic import で循環依存を回避)。
+
+##### Service Interface
+```typescript
+export async function getApplicationResourceAttributes(): Promise<Attributes>;
+// 戻り値: { 'growi.service.type': string, 'growi.deployment.type': string }
+```
+
+### Layer 3: Custom Metric
+
+#### ApplicationMetrics
+
+| Field | Detail |
+|-------|--------|
+| Intent | GROWI 設定情報を info-gauge `growi.configs` のラベルに集約 |
+| Requirements | 3.1, 3.2, 3.3, 3.4, 3.5 |
+
+**Responsibilities & Constraints**
+- `growi.configs` ObservableGauge(unit `'1'`、値は常に 1)を 1 個 emit する。
+- ラベル: `site_url`, `site_url_hashed?`, `wiki_type`, `external_auth_types`, `attachment_type`。
+- `otel:isAppSiteUrlHashed === true` のとき `site_url = '[hashed]'`、`site_url_hashed = SHA-256(appSiteUrl)`。`false` のとき生 URL + `site_url_hashed = undefined`。
+- `external_auth_types` / `attachment_type` の値が未取得時は空文字 `''`。
+
+**Dependencies**
+- Outbound: `growiInfoService.getGrowiInfo({ includeAttachmentInfo: true })`, `configManager.getConfig('otel:isAppSiteUrlHashed')`.
+
+##### Service Interface
+```typescript
+export function addApplicationMetrics(): void;
+```
+
+##### Label Schema: `growi.configs`
+| Label | Source | Notes |
+|-------|--------|-------|
+| `site_url` | `isAppSiteUrlHashed ? '[hashed]' : growiInfo.appSiteUrl` | required |
+| `site_url_hashed` | `isAppSiteUrlHashed ? sha256(appSiteUrl) : undefined` | hashed 時のみ付与 |
+| `wiki_type` | `growiInfo.wikiType` | required |
+| `external_auth_types` | `additionalInfo?.activeExternalAccountTypes?.join(',') \|\| ''` | required(カンマ区切り) |
+| `attachment_type` | `additionalInfo?.attachmentType ?? ''` | required |
+
+#### UserCountsMetrics
+
+| Field | Detail |
+|-------|--------|
+| Intent | GROWI 上のユーザー数とアクティブユーザー数の継続観測 |
+| Requirements | 4.1, 4.2, 4.4 |
+
+**Responsibilities & Constraints**
+- `growi.users.total` / `growi.users.active` の 2 つの ObservableGauge(unit `'users'`)。
+- `growiInfoService.getGrowiInfo({ includeUserCountInfo: true })` を呼び、`additionalInfo.currentUsersCount` / `currentActiveUsersCount` をそれぞれ observe。未取得時は 0。
+
+##### Service Interface
+```typescript
+export function addUserCountsMetrics(): void;
+```
+
+#### PageCountsMetrics
+
+| Field | Detail |
+|-------|--------|
+| Intent | GROWI 上の総ページ数の継続観測 |
+| Requirements | 4.3, 4.4 |
+
+**Responsibilities & Constraints**
+- `growi.pages.total`(unit `'pages'`)の ObservableGauge 1 つ。
+- `growiInfoService.getGrowiInfo({ includePageCountInfo: true })` の `additionalInfo.currentPagesCount` を observe。未取得時は 0。
+
+##### Service Interface
+```typescript
+export function addPageCountsMetrics(): void;
+```
+
+#### SystemMetrics
+
+| Field | Detail |
+|-------|--------|
+| Intent | コンテナ / ホスト / プロセス / V8 ヒープのメモリ系統計を ObservableGauge で出力 |
+| Requirements | 5.1, 5.2, 5.3, 5.4, 5.5 |
+
+**Responsibilities & Constraints**
+- 単一 Meter `growi-system-metrics`(version `'1.0.0'`)で 6 つの ObservableGauge を作成。すべて単位 `'By'`。
+- 1 つの `addBatchObservableCallback` で `process.constrainedMemory()` / `os.totalmem()` / `process.memoryUsage()` / `v8.getHeapStatistics()` を 1 回ずつ呼び、ローカル変数経由で 6 個の gauge に観測値を割り振る。
+- `process.constrainedMemory()` が `> 0` のときのみ `system.memory.limit` を観測、`0` または falsy のときは当該 gauge のみスキップし他 5 個は観測する。
+- コールバック全体を try/catch で囲み、例外時は `loggerDiag.error('Failed to collect system metrics', { error })` を呼んで `result.observe` を一切呼ばずに return。
+
+##### Metric Schema
+| Metric Name | Unit | Source | Skip Condition |
+|-------------|------|--------|----------------|
+| `system.memory.limit` | `By` | `process.constrainedMemory()` | 値が `0` または falsy |
+| `system.host.memory.total` | `By` | `os.totalmem()` | — |
+| `process.memory.usage` | `By` | `process.memoryUsage().rss` | — |
+| `process.runtime.v8.heap.used` | `By` | `v8.getHeapStatistics().used_heap_size` | — |
+| `process.runtime.v8.heap.total` | `By` | `v8.getHeapStatistics().total_heap_size` | — |
+| `process.runtime.v8.heap.external` | `By` | `process.memoryUsage().external` | — |
+
+##### Service Interface
+```typescript
+export function addSystemMetrics(): void;
+```
+
+**Implementation Notes**
+- `process.constrainedMemory()` は Node.js 19.6 で導入 / 20.12 で stable。`apps/app` の `engines.node` は `^24` のためサポートされるが、防御的に `(process as NodeJS.Process & { constrainedMemory?(): number }).constrainedMemory?.() ?? 0` でアクセスする。
+- API 呼び出しの重複を避けるため、各 stdlib API は callback 内で 1 回ずつのみ呼ぶ。
+
+#### CustomMetricsIndex
+
+| Field | Detail |
+|-------|--------|
+| Intent | Custom Metric モジュール群の起動合成点 |
+| Requirements | 4.1–4.4, 10.1 |
+
+**Responsibilities & Constraints**
+- 各モジュールを dynamic import し、`addApplicationMetrics()` / `addUserCountsMetrics()` / `addPageCountsMetrics()` / `addSystemMetrics()` を順次呼ぶ。
+- 新規モジュール追加時はこの順序の末尾に append する(既存ダッシュボードに影響しない)。
+
+##### Service Interface
+```typescript
+export const setupCustomMetrics: () => Promise<void>;
+export { addApplicationMetrics, addPageCountsMetrics, addSystemMetrics, addUserCountsMetrics };
+```
+
+### Layer 4: HTTP Anonymization
+
+#### HttpInstrumentationConfig
+
+| Field | Detail |
+|-------|--------|
+| Intent | `@opentelemetry/instrumentation-http` の `startIncomingSpanHook` に注入し、登録された anonymization module を順次評価する |
+| Requirements | 6.1, 6.5 |
+
+**Responsibilities & Constraints**
+- `startIncomingSpanHook(request)` で URL を取り出し、`anonymizationModules` を `canHandle(url)` で順次フィルタ、マッチした module の `handle(request, url)` の戻り値(`{ [ATTR_HTTP_TARGET]: <anonymized> }` または `null`)を `Object.assign` で集約。
+- 注入は `node-sdk-configuration.ts` 経由で、`otel:anonymizeInBestEffort` が `true` のときのみ行う。
+
+##### Service Interface
+```typescript
+export const httpInstrumentationConfig: InstrumentationConfigMap['@opentelemetry/instrumentation-http'];
+```
+
+#### AnonymizationModule(interface)
+
+| Field | Detail |
+|-------|--------|
+| Intent | 個別 anonymization handler の共通契約 |
+| Requirements | 6.1, 6.2, 6.3, 6.4, 10.2 |
+
+##### Interface
+```typescript
+export interface AnonymizationModule {
+  canHandle(url: string): boolean;
+  handle(request: IncomingMessage, url: string): Record<string, string> | null;
+}
+```
+
+#### AnonymizationHandlersIndex
+
+| Field | Detail |
+|-------|--------|
+| Intent | 登録済み handler のコレクション。**配列順 = 評価順**で、より具体的なパスから書く。 |
+| Requirements | 6.2, 6.3, 6.4, 10.2 |
+
+**Registration Order**
+1. `searchApiModule` — 検索 API
+2. `pageListingApiModule` — page-listing API
+3. `pageApiModule` — pages/list 系 API
+4. `pageAccessModule` — 非 API ページアクセス(最も汎用的なため最後)
+
+#### SearchApiModule
+
+| Field | Detail |
+|-------|--------|
+| Intent | `/_api/search`, `/_search` の `q` クエリパラメータを匿名化 |
+| Requirements | 6.2 |
+
+**canHandle**: `/\/_api\/search(\?|$)/` または `/\/_search(\?|$)/` または `'/_api/search/'` / `'/_search/'` を含む。
+**handle**: `q` パラメータが含まれていれば `anonymizeQueryParams(url, ['q'])`。
+
+#### PageListingApiModule
+
+| Field | Detail |
+|-------|--------|
+| Intent | `/_api/v3/page-listing/{ancestors-children,children,item}` の `path` パラメータを匿名化 |
+| Requirements | 6.3 |
+
+#### PageApiModule
+
+| Field | Detail |
+|-------|--------|
+| Intent | `/_api/v3/pages/{list,subordinated-list}` および `/_api/v3/page/{check-page-existence,get-page-paths-with-descendant-count}` の `path` / `paths` パラメータを匿名化 |
+| Requirements | 6.3 |
+
+#### PageAccessModule
+
+| Field | Detail |
+|-------|--------|
+| Intent | API 以外のページアクセスのうち、`isCreatablePage` を満たすパスのみ匿名化。permalink(ObjectId)は素通し、user ページはユーザー名と残りパスを別々にハッシュ。 |
+| Requirements | 6.4 |
+
+**Behavior**:
+- ルート `/`、静的リソース(`/static/`, `/_next/`, `/favicon`, `/assets/`, 拡張子付き)、`/user`(users top page)、permalink を除外。
+- user page (`/user/<name>/...`) はユーザー名と残りパスを SHA-256 prefix(16 文字)で別々にハッシュ → `/user/[USERNAME_HASHED:<hash>][/?][HASHED:<hash>]`。
+- それ以外の通常 page はパス全体を SHA-256 prefix で 1 ハッシュ → `[HASHED:<hash>]`。
+
+#### AnonymizeQueryParams(utility)
+
+| Field | Detail |
+|-------|--------|
+| Intent | クエリパラメータの値を `[ANONYMIZED]` リテラルに置換する純粋関数 |
+| Requirements | 6.2, 6.3 |
+
+##### Service Interface
+```typescript
+export function anonymizeQueryParams(target: string, paramNames: string[]): string;
+```
+
+**Behavior**:
+- 通常パラメータは `[ANONYMIZED]` で置換。値が JSON 配列フォーマットなら `["[ANONYMIZED]"]` を返す。
+- `paramName[]` 形式の配列パラメータには `[ANONYMIZED]` を 1 つだけ残す。
+- 変更が無ければ入力をそのまま返す(無駄な URL 再構築を避ける)。
+
+### Layer 5: SemConv ローカルコピー
+
+#### SemConv
+
+| Field | Detail |
+|-------|--------|
+| Intent | OpenTelemetry incubating semconv を文字列定数として固定化 |
+| Requirements | 9.1, 9.2 |
+
+##### Definitions
+```typescript
+export const ATTR_SERVICE_INSTANCE_ID = 'service.instance.id';
+export const ATTR_HTTP_TARGET = 'http.target';
+```
+
+**Implementation Notes**
+- 上流の incubating entry-point は import 禁止。stable 化されたら本ファイルから削除し、stable 定数の import に切り替える(Revalidation Trigger)。
+
+## Error Handling
+
+### Error Strategy
+
+各レイヤで発生する例外は **その場で吸収し、上位レイヤや他メトリクスを巻き込まない** ことを基本方針とする。
+
+- **Layer 1(SDK lifecycle)**: SDK 構築失敗時のみ throw を許容(起動を継続できないため)。Resource 取得失敗は warn ログでスキップ。
+- **Layer 2(Resource Attribute)**: `growiInfoService` 失敗時は try/catch で空 `Attributes` を返す(SDK 起動は継続可能)。
+- **Layer 3(Custom Metric)**: 各 `addBatchObservableCallback` 内で try/catch。例外時は `diag.createComponentLogger(...).error(...)` を呼び、その collection cycle では observe を 1 回も呼ばない。次の cycle で再試行。
+- **Layer 4(Anonymization)**: 各 handler の `handle` / `canHandle` で try/catch、失敗時は anonymization をスキップし元の URL のまま span 属性に乗せる(または何もしない)。
+
+### Error Categories and Responses
+
+| Category | 例 | 振る舞い |
+|----------|-----|---------|
+| 起動時 SDK 構築失敗 | `OTLPTraceExporter()` のコンストラクタ例外 | プロセス起動継続不可。throw を上位に伝播。 |
+| Resource 取得失敗 | `growiInfoService.getGrowiInfo()` 例外 | `logger.error` で記録し空 `Attributes` 返却。 |
+| Metric collection 例外 | `growiInfoService` 失敗、stdlib 失敗(理論上発生しない) | `loggerDiag.error` で記録、当該 cycle の observe をスキップ。 |
+| Anonymization 失敗 | URL parse 失敗、handler 内部例外 | `diag` logger に warn / debug、URL は元のまま。 |
+| `process.constrainedMemory()` の戻り値が 0 | 非コンテナ環境 | `system.memory.limit` のみスキップ。他 5 メトリクスは observe。 |
+
+### Monitoring
+
+- Diag ログ namespace 規約:
+  - `growi:custom-metrics:application`
+  - `growi:custom-metrics:user-counts`
+  - `growi:custom-metrics:page-counts`
+  - `growi:custom-metrics:system`
+  - `growi:anonymization:<handler-name>`
+- アプリケーションログ(pino): `loggerFactory('growi:opentelemetry:<sub-namespace>')` で起動完了メッセージを info ログ出力。
+
+## Testing Strategy
+
+### Unit Tests
+
+各モジュールに `*.spec.ts` を co-locate する。テスト設計の指針:
+
+- **Layer 2 spec**(`os-resource-attributes.spec.ts`, `application-resource-attributes.spec.ts`): `vi.mock('node:os')` で stdlib を、`vi.mock` で `growiInfoService` をモックし、戻り値 attributes のキー集合を assert。
+- **Layer 3 spec**: `vi.mock('@opentelemetry/api')` で `metrics`, `diag` をモック、`vitest-mock-extended` の `mock<Meter>()` / `mock<ObservableGauge>()` で gauge を取得し、`meter.addBatchObservableCallback.mock.calls[0][0]` でコールバックを取り出して直接実行する。`result.observe` のモックを assert する。
+- **Layer 4 spec**: handler ごとに `canHandle` の境界条件(先頭一致 / クエリ有無 / 静的リソース除外 / permalink 除外)と `handle` の URL 変換を網羅。`anonymize-query-params.spec.ts` で JSON 配列 / `paramName[]` フォーマットを網羅。
+- **SystemMetrics spec**: `vi.mock('node:os')`, `vi.mock('node:v8')`, `vi.spyOn(process, 'constrainedMemory')`, `vi.spyOn(process, 'memoryUsage')` を組み合わせる。
+
+### Integration Tests
+
+- `node-sdk.spec.ts` が SDK 構築・初期化シーケンスを統合的に検証。
+- `node-sdk.testing.ts` がテストヘルパとして共通利用される。
+
+### Manual / E2E Verification
+
+- 開発 devcontainer で `OTEL_EXPORTER_OTLP_ENDPOINT` を `http://localhost:4317` 等に向け、Collector の receiver ログで以下を確認:
+  - Resource Attribute が identity セット 8 種のみであること。
+  - `growi.configs` の `attachment_type` ラベルが期待値(`aws` / `gcs` / `gridfs` / `local` / `mongodb` / `azure` 等)または空文字であること。
+  - `system.host.memory.total` / `process.memory.usage` / `process.runtime.v8.heap.*` が約 5 分間隔で届くこと。
+  - Docker container で `--memory=512m` を指定した場合に `system.memory.limit` が約 `536870912`、未指定時は emit されないこと。
+- `OPENTELEMETRY_ANONYMIZE_IN_BEST_EFFORT=true` のとき、検索 / 編集等の操作後 span の `http.target` が `[ANONYMIZED]` / `[HASHED:...]` で置換されていること。

+ 139 - 0
.kiro/specs/opentelemetry/requirements.md

@@ -0,0 +1,139 @@
+# Requirements Document
+
+## Introduction
+
+GROWI の OpenTelemetry 統合 (`apps/app/src/features/opentelemetry/`) を **メンテナンスするための大局的な仕様**。SDK ライフサイクル、Resource Attribute、Custom Metric、HTTP Anonymization の 4 レイヤがそれぞれ「何を担い、何を担わないか」を明文化し、新規メトリクスや anonymization handler の追加、SDK のバージョンアップ、設定キーの追加・改名といった将来のメンテナンス時に、本 spec を 1 か所の参照点として運用できる状態を目標とする。
+
+本 spec は新規実装 spec ではなく、既に実装・稼働している `features/opentelemetry/` の **現状の責務境界をスナップショットとして固定化する** 性格を持つ。個別機能の追加・変更は原則として本 spec の Boundary Commitments の範囲内で行われ、境界をまたぐ変更が必要なときは Revalidation Triggers として再評価される。
+
+## Boundary Context
+
+- **In scope**:
+  - NodeSDK の起動・有効化制御・Resource 2 段階初期化 (`node-sdk.ts`, `node-sdk-configuration.ts`, `node-sdk-resource.ts`)。
+  - Diag Logger の pino アダプタ (`logger.ts`)。
+  - SemConv の不安定 attribute のローカルコピー (`semconv.ts`)。
+  - identity 専用の Resource Attribute 供給 (`custom-resource-attributes/`)。
+  - Custom Metric の emit と合成 (`custom-metrics/`、合計 4 モジュール: application / user-counts / page-counts / system)。
+  - HTTP リクエストの best-effort anonymization (`anonymization/`、4 個の handler + utility)。
+  - `otel:*` 設定キー 4 種の利用ポリシー。
+- **Out of scope**:
+  - `growiInfoService` / `configManager` / `loggerFactory` などの上流サービスの設計や API 変更。
+  - 既存メトリクス(`growi.users.total` / `growi.users.active` / `growi.pages.total` / `growi.configs` / `system.*` / `process.*`)の名称変更や再構成。
+  - OpenTelemetry のログシグナル統合(log signal は現状未使用)。
+  - クライアント側(ブラウザ)からの telemetry 出力。
+  - Trace span への独自 attribute 追加(`http.target` 以外)。
+  - OTLP Exporter の wire 仕様や受信側ツールチェイン。
+- **Adjacent expectations**:
+  - 上流: `growiInfoService.getGrowiInfo({ includeAttachmentInfo, includeUserCountInfo, includePageCountInfo })` の API シグネチャ・返り値型が維持されることに依存する。破壊的変更があった場合は Revalidation Triggers として `custom-metrics/` および `custom-resource-attributes/application-resource-attributes.ts` を再評価する。
+  - 上流: `configManager.getConfig('otel:*')` の 4 キーが現状の意味で参照可能であることに依存する。
+  - 下流: OpenTelemetry Collector およびその先のダッシュボード/アラート群が本 spec の Metric Schema / Resource Attribute 表に整合した参照クエリを保持していること。変更時は PR 説明にて運用者へ通知する。
+
+## Requirements
+
+### Requirement 1: SDK ライフサイクルと有効化制御
+
+**Objective:** GROWI 運用者として、OpenTelemetry SDK を環境変数 1 つで有効/無効を切り替えられ、無効時にはランタイムオーバーヘッドや誤った OTLP 接続試行が発生しないことを保証したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall `otel:enabled` 設定が `false` のとき、NodeSDK インスタンスを生成せず Resource Attribute 取得や Custom Metric の登録も行わない。
+2. The GROWI server shall `otel:enabled` 設定値と `OTEL_SDK_DISABLED` 環境変数の値が矛盾している場合、`OTEL_SDK_DISABLED` を上書きして整合性を取り、その旨を warn ログとして出力する。
+3. The GROWI server shall NodeSDK 初期化を「SDK 構築・Resource 静的部分のセット」「DB 初期化後の Resource 追加注入」「`start()` 呼び出しと Custom Metric 登録」の 3 段階で行う。各段階は `otel:enabled` を再確認した上で実行する。
+4. The GROWI server shall 同一プロセス内で `initInstrumentation()` を二重に呼ばれても、SDK インスタンスを重複生成しない(再初期化は警告を出してスキップする)。
+
+### Requirement 2: identity 専用 Resource Attribute
+
+**Objective:** 受信側インフラ管理者として、GROWI が emit する Resource Attribute がテレメトリ発生元エンティティの identity 情報のみであり、測定値や設定値が紛れ込まないことを保証したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall Resource Attribute として以下のみを emit する: `service.name`, `service.version`, `service.instance.id`(取得できた場合), `os.type`, `os.platform`, `os.arch`, `growi.service.type`, `growi.deployment.type`。
+2. The GROWI server shall 測定値(メモリ使用量・カウント等)および GROWI のサブシステム設定値(attachment / auth provider 種別等)を Resource Attribute として emit しない。
+3. The GROWI server shall Resource Attribute の取得を 2 段階に分け、1 段階目は DB 非依存(service.name / version / OS info)、2 段階目は DB 初期化後(`service.instance.id` / `growi.service.type` / `growi.deployment.type`)に行う。
+4. If Resource Attribute 取得処理で例外が発生した場合, the GROWI server shall 当該段階の Resource Attribute を空オブジェクトで返し、SDK 起動自体は継続する。
+
+### Requirement 3: GROWI 設定情報の info-gauge ラベル統合
+
+**Objective:** 運用者として、GROWI インスタンスの設定情報(site URL、wiki type、外部認証種別、添付ストレージ種別)を 1 つの info-gauge メトリクスから一覧できることを保証したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall `growi.configs` という ObservableGauge を Prometheus info パターン(値は常に 1、情報はラベルに格納)で emit する。
+2. The GROWI server shall `growi.configs` に以下のラベルを付与する: `site_url`, `site_url_hashed`, `wiki_type`, `external_auth_types`, `attachment_type`。
+3. If `otel:isAppSiteUrlHashed` が `true`, the GROWI server shall `site_url` を `[hashed]` リテラルにし、`site_url_hashed` に SHA-256 ハッシュ値を入れる。`false` のときは `site_url` に生 URL を入れ `site_url_hashed` は `undefined`(emit されない)。
+4. If `external_auth_types` / `attachment_type` の値が `growiInfoService` から取得できない場合, the GROWI server shall 当該ラベルを空文字 `''` でフォールバックする。
+5. The GROWI server shall ラベル名を snake_case で統一する。
+
+### Requirement 4: 業務カウントメトリクス
+
+**Objective:** 運用者として、GROWI 上の主要エンティティ(ユーザー、ページ)の総数とアクティビティ指標を継続的に観測したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall `growi.users.total` メトリクスを総ユーザー数で観測する(単位 `users`)。
+2. The GROWI server shall `growi.users.active` メトリクスをアクティブユーザー数で観測する(単位 `users`)。
+3. The GROWI server shall `growi.pages.total` メトリクスを総ページ数で観測する(単位 `pages`)。
+4. If `growiInfoService` からのカウント値取得が失敗した場合, the GROWI server shall 0 で観測するか、当該収集サイクルでの観測をスキップし、`diag` ロガーに error を記録する。
+
+### Requirement 5: コンテナ運用に対応したメモリ系メトリクス
+
+**Objective:** コンテナ環境(Docker / Kubernetes)で GROWI を運用する管理者として、「コンテナに割り当てられたメモリ上限(cgroup limit)」「ホスト物理メモリ総量」「プロセス RSS」「V8 ヒープの使用/確保/外部メモリ」を別々のメトリクスとして観測できることを保証したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall `system.memory.limit` を `process.constrainedMemory()` の戻り値(>0 のとき)で観測する。値が `0` または falsy のときは当該メトリクスのみ観測をスキップする。
+2. The GROWI server shall `system.host.memory.total` を `os.totalmem()` の戻り値で常に観測する。
+3. The GROWI server shall `process.memory.usage` を `process.memoryUsage().rss` で観測する。
+4. The GROWI server shall `process.runtime.v8.heap.used` / `process.runtime.v8.heap.total` / `process.runtime.v8.heap.external` を `v8.getHeapStatistics()` および `process.memoryUsage().external` から観測する。
+5. The GROWI server shall 上記すべてのメトリクスを単位 `By`(bytes)で emit する。
+
+### Requirement 6: HTTP リクエストの best-effort anonymization
+
+**Objective:** プライバシ保護担当者として、`otel:anonymizeInBestEffort` が `true` のとき、ユーザーが入力した検索キーワード・ページパス・ユーザー名がトレース span の `http.target` に平文で残らないことを保証したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall `otel:anonymizeInBestEffort` が `true` のとき、HTTP instrumentation の `startIncomingSpanHook` で `anonymizationModules` を順次評価し、`canHandle(url)` が `true` を返した module の `handle()` 結果を span attribute としてマージする。
+2. The GROWI server shall 検索 API (`/_api/search`, `/_search`) の `q` クエリパラメータを `[ANONYMIZED]` に置換する。
+3. The GROWI server shall page-listing API (`/_api/v3/page-listing/{ancestors-children,children,item}`) および page API (`/_api/v3/pages/list`, `/_api/v3/pages/subordinated-list`, `/_api/v3/page/check-page-existence`, `/_api/v3/page/get-page-paths-with-descendant-count`) の `path` / `paths` パラメータを匿名化する。
+4. The GROWI server shall ページアクセス(非 API、permalink でない、創出可能なページパス)に対し、ユーザー名およびページパスを SHA-256 prefix(16 文字)でハッシュし `[USERNAME_HASHED:...]` / `[HASHED:...]` プレースホルダで置換する。permalink(ObjectId)と users top page(`/user`)はそのまま残す。
+5. If `otel:anonymizeInBestEffort` が `false`, the GROWI server shall `startIncomingSpanHook` を渡さず、HTTP instrumentation の標準動作のみを行う。
+
+### Requirement 7: Diag Logger と pino の統合
+
+**Objective:** 開発者として、OpenTelemetry 内部の `diag` ログが GROWI の通常のアプリケーションログ(pino)と同一フォーマット・同一出力先で観測できることを保証したい。
+
+#### Acceptance Criteria
+
+1. When `NODE_ENV === 'development'` かつ `otel:enabled` が `true`, the GROWI server shall `DiagLogger` を pino logger にアダプトする実装 (`DiagLoggerPinoAdapter`) をグローバルに登録する。
+2. The GROWI server shall `diag.error/warn/info/debug/verbose` で受け取ったメッセージが JSON 文字列の場合に parse して構造化 data に変換し、pino の引数規約(data 第 1 引数・message 第 2 引数)に整合する形で渡す。
+3. The GROWI server shall production 環境では `initLogger()` を呼ばない(OpenTelemetry の `diag` 既定動作に委ねる)。
+
+### Requirement 8: メトリクスエクスポートと SDK 設定
+
+**Objective:** 運用者として、OTLP メトリクス/トレースエクスポートが OpenTelemetry SDK 標準の環境変数(`OTEL_EXPORTER_OTLP_ENDPOINT` 等)で制御でき、内部で勝手な default endpoint が固定されないことを保証したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall `OTLPTraceExporter` および `OTLPMetricExporter` をコンストラクタ引数なしで生成し、エンドポイントなど exporter 設定は OpenTelemetry SDK 標準の環境変数で解決させる。
+2. The GROWI server shall `PeriodicExportingMetricReader` の `exportIntervalMillis` を 300000(5 分)で初期化する。
+3. The GROWI server shall auto-instrumentation のうち `@opentelemetry/instrumentation-pino` および `@opentelemetry/instrumentation-fs` を明示的に無効化する(pino: log signal を使用しないため、fs: トレース量が膨大すぎるため)。
+
+### Requirement 9: SemConv の不安定 attribute のローカルコピー
+
+**Objective:** 開発者として、`@opentelemetry/semantic-conventions` の incubating attribute をランタイムコードから直接 import せず、本モジュール内のローカルコピーを参照することで、上流の minor リリースでの破壊的変更からアプリケーションを保護したい。
+
+#### Acceptance Criteria
+
+1. The GROWI server shall incubating attribute(`service.instance.id`, `http.target`)を `semconv.ts` 内に文字列定数として定義し、ランタイムコードはこれを import する。
+2. The GROWI server shall `@opentelemetry/semantic-conventions/incubating` からの import をランタイムコードに含めない。
+
+### Requirement 10: 拡張・追加時の境界遵守
+
+**Objective:** 機能を追加・変更するエンジニアとして、新規 Custom Metric や新規 Anonymization Handler を本 spec の境界に従って実装し、レイヤ責務の汚染を回避したい。
+
+#### Acceptance Criteria
+
+1. When 新規 Custom Metric モジュールを追加する, the GROWI server shall `custom-metrics/` 配下にファイルを追加し、`addXxxMetrics(): void` をエクスポートし、`custom-metrics/index.ts` の `setupCustomMetrics()` から呼び出す。
+2. When 新規 Anonymization Handler を追加する, the GROWI server shall `anonymization/handlers/` 配下に `AnonymizationModule` 実装ファイルを追加し、`handlers/index.ts` の `anonymizationModules` 配列に登録する。
+3. The GROWI server shall identity 情報を Resource Attribute 経由で、設定値を `growi.configs` ラベル経由で、観測値を `growi.*` / `system.*` / `process.*` メトリクス経由でそれぞれ emit する責務分離を維持する。

+ 173 - 0
.kiro/specs/opentelemetry/research.md

@@ -0,0 +1,173 @@
+# Research & Design Decisions — opentelemetry
+
+## Summary
+
+- **Feature**: `opentelemetry`(`apps/app/src/features/opentelemetry/` の大局的メンテナンス spec)。
+- **Discovery Scope**: Extension/Refactor — 既存実装を保ったまま、4 レイヤの責務境界を明文化する。
+
+## Research Log
+
+### 既存 ObservableGauge 実装パターン
+
+- **Context**: 新規 Custom Metric を追加するときに既存パターンと整合させる必要がある。
+- **Sources Consulted**:
+  - `custom-metrics/application-metrics.ts`, `user-counts-metrics.ts`, `page-counts-metrics.ts`, `system-metrics.ts`。
+- **Findings**:
+  - 各モジュールは `addXxxMetrics(): void` を export する。
+  - `metrics.getMeter('growi-<scope>-metrics', '1.0.0')` で Meter を取得し、`meter.createObservableGauge(name, { description, unit })` で gauge を作る。
+  - 観測は `meter.addBatchObservableCallback(async (result) => { try { ... } catch (e) { loggerDiag.error(...) } }, [gauge, ...])` で登録。
+  - ロガー初期化: `loggerFactory('growi:opentelemetry:custom-metrics:<scope>')`(pino)と `diag.createComponentLogger({ namespace: 'growi:custom-metrics:<scope>' })`(OTel diag)の 2 つ。
+- **Implications**: 拡張テンプレートとしてこのパターンを design.md に記載済み(File Structure Plan の "Extension Templates")。
+
+### Anonymization Handler の登録順とパターン
+
+- **Context**: 新規 anonymization handler を追加するとき、`canHandle` の衝突を避ける必要がある。
+- **Sources Consulted**: `anonymization/handlers/index.ts`、各 handler の `canHandle` 実装。
+- **Findings**:
+  - 配列順 = 評価順だが、すべてが OR で集約される(複数 module が同一 URL を匿名化することは現状無いが、可能性としては存在する)。
+  - より具体的なパス(API 系)を先、汎用パス(page access)を最後に配置するのが現状の慣習。
+  - `canHandle` は副作用無しで判定のみ、`handle` は失敗時に `null` を返すか元の URL を維持する。
+- **Implications**: 新規 handler 追加時は、既存 4 handler の対象 URL と衝突しないかを `canHandle` ロジックで確認する。
+
+### `process.constrainedMemory()` の挙動
+
+- **Context**: コンテナ環境とそれ以外で挙動が異なるため、`system.memory.limit` の skip 条件を確定する必要がある。
+- **Sources Consulted**: Node.js v20.12 / v24 公式ドキュメント。
+- **Findings**:
+  - 戻り値: cgroup v1 / v2 から取得した「プロセスに割り当てられたメモリ上限のバイト数」。
+  - cgroup が未設定 / detection 失敗時 / macOS・Windows では `0` を返す(v24 でも継続)。
+  - Node.js v19.6 で導入、v20.12 で stable。
+- **Implications**: `value > 0`(falsy)で判定すれば、macOS・Windows・cgroup なし Linux すべてで一貫した「skip」挙動になる。
+
+### NodeSDK `_resource` への private アクセス
+
+- **Context**: 2 段階目の Resource を NodeSDK に注入する必要があり、public API が見当たらない。
+- **Sources Consulted**: `@opentelemetry/sdk-node` の TypeScript 型定義、`node-sdk-resource.ts`。
+- **Findings**:
+  - NodeSDK は constructor で受け取った resource を内部に保持するが、外部から書き換える public API は存在しない(`sdk-node 0.217.0` 時点)。
+  - `_resource` プロパティを直接書き換えることで、`start()` 前に Resource を差し替えられる。
+- **Implications**: `(sdk as any)._resource` への reflective アクセスを `getResource` / `setResource` で隔離。SDK のメジャー更新時に public API が出ていないか Revalidation Trigger として確認する。
+
+## Architecture Pattern Evaluation
+
+| Option | Description | Strengths | Risks / Limitations | Notes |
+|--------|-------------|-----------|---------------------|-------|
+| Custom ObservableGauge per layer | 自前で 4 Meter / 7+ gauge を実装し、`@opentelemetry/host-metrics` を採用しない | 完全制御、cgroup / V8 対応、追加 dep ゼロ、Meter ごとに spec 単位でテスト可能 | コード量増(〜500 行) | **採用** |
+| `@opentelemetry/host-metrics` 採用 | system / process メトリクスをコミュニティパッケージで自動 emit | 既製、ネットワーク・CPU も追加 | cgroup 未対応、V8 ヒープ非対応、不要メトリクス強制 emit、semconv 古い | 不採用(要件 5 未充足) |
+| Single Meter, all metrics | 全 7+ メトリクスを単一 Meter で束ねる | コードが小さい | 観測スコープ(business vs system)の責務が混在、テスト分離困難 | 不採用 |
+| 2-stage Resource initialization | DB 非依存 → DB 初期化後 の 2 段階で Resource を構築 | 循環依存回避、DB 接続前に SDK 部分起動可能 | `_resource` private アクセス必要 | **採用** |
+| Single-stage Resource | すべての Resource を DB 初期化後に作る | private アクセス不要 | OpenTelemetry の起動が DB 接続まで遅延、`service.name` などの基本属性も遅れる | 不採用 |
+| Module-based anonymization | `AnonymizationModule` interface + 配列順評価 | 新規パス追加が局所変更で済む、handler ごとに spec | 配列順への暗黙依存 | **採用** |
+| Centralized anonymization (switch / regex map) | 1 ファイルで if/else または map で振り分け | フローが見やすい | 拡張ごとに 1 ファイルが肥大化、spec が結合 | 不採用 |
+
+## Design Decisions
+
+### Decision: 4 レイヤの責務分離(identity / 設定 / 観測 / anonymization)
+
+- **Context**: Resource Attribute / Metric / Span Attribute それぞれの本来の用途を運用ガイドラインとして固定したい。
+- **Selected Approach**: 以下の 4 分類で責務を分離する。
+  - **identity**(不変または起動時固定) → Resource Attribute
+  - **設定値**(インスタンス設定の確認用、ラベル次元として参照する) → `growi.configs` info gauge ラベル
+  - **観測値**(時間と共に変化するスカラー) → `growi.*` / `system.*` / `process.*` ObservableGauge
+  - **span attribute**(リクエスト単位の情報、必要なら匿名化) → `http.target` 等 incubating semconv
+- **Rationale**: OpenTelemetry の data model(Resource / Metric / Span)に対する公式の意味論に沿う。Resource に measurement や設定値を載せると receiving side でカーディナリティ爆発・誤った集計の原因になる(特に Resource に乗ったホストメモリ量はコンテナ環境で「ホストの値」を返してしまい運用上の判断を誤らせる典型例)。
+- **Trade-offs**: 設計時の判断分岐が増えるが、ダッシュボード保守の堅牢性が大きく上がる。
+
+### Decision: `system.memory.limit` と `system.host.memory.total` を別メトリクスに分離
+
+- **Context**: コンテナ環境で「コンテナの上限」と「ホストの物理メモリ」のどちらを参照したいかは運用観点が異なる。
+- **Alternatives Considered**:
+  1. 単一メトリクス `system.memory.limit` を cgroup → fallback で `os.totalmem` にする。
+  2. `system.memory.limit` と `system.host.memory.total` を別メトリクスにする。
+- **Selected Approach**: 2。`system.memory.limit` は cgroup limit が取れたときのみ観測、`system.host.memory.total` は常に観測。
+- **Rationale**: 「コンテナ上限の有無」自体が運用上の情報。fallback されると bare-metal でも cgroup でも同じシリーズに混在し、ダッシュボードで見分けが付かない。
+- **Trade-offs**: 出力メトリクス数が 1 つ増えるが、運用観点での明瞭さが勝る。
+- **Follow-up**: ダッシュボード移行時の運用者向け説明に「cgroup limit 未設定では `system.memory.limit` が emit されない」を明記する。
+
+### Decision: サブシステム設定値(`attachment.type` 等)は `growi.configs` のラベルへ統合
+
+- **Context**: GROWI インスタンスの設定値(`wiki_type`, `external_auth_types`, `attachment_type` 等)を Resource Attribute に載せるか、専用 info-gauge のラベルに載せるかという選択。
+- **Alternatives Considered**:
+  1. Resource Attribute として emit する。
+  2. `growi.configs` ObservableGauge(値は常に 1)のラベルへ統合(Prometheus info パターン)。
+  3. 設定値ごとに独立した info gauge を新設する。
+- **Selected Approach**: 2。snake_case 統一の単一 info-gauge のラベル群として集約する。
+- **Rationale**: identity(Resource)と設定値を分離することで Resource を「テレメトリ発生元の不変識別子」として清潔に保てる。複数の設定値を 1 つの info-gauge に集約することで「インスタンス設定を 1 か所で見られる」運用が成立する。
+- **Trade-offs**: `growi.configs` のラベル数は機能追加と共に増える。各値が固定 enum 由来のためカーディナリティ影響は限定的。
+- **Follow-up**: 値の取得不能時は空文字 `''` フォールバックで統一する(`undefined` ラベル attribute が emit されないことを利用しない)。
+
+### Decision: `growi.deployment.type` は OTel 標準 `deployment.environment.name` に寄せない
+
+- **Context**: OTel 標準には `deployment.environment.name`("production"/"staging" 等)があるが、GROWI の `growi.deployment.type`("docker"/"k8s"/"growi-docker-compose" 等)はランタイム形態を表し、環境分類とは別概念。
+- **Selected Approach**: `growi.deployment.type` のまま据え置く(Resource Attribute)。
+- **Rationale**: 値の意味が semconv 標準と乖離するため、無理に標準名を当てると誤解を招く。
+- **Follow-up**: 将来的に「環境(prod/stg)」の表現が必要になった時点で、別途 `deployment.environment.name` を追加導入する。
+
+### Decision: 単一 Meter `growi-system-metrics` で system / process / V8 を束ねる
+
+- **Context**: 既存パターンでは目的別に Meter を分けている(application / user-counts / page-counts)。System / Process / V8 のメトリクス群も同様に分けるか統合するかの判断が必要。
+- **Selected Approach**: System / Process / V8 を `growi-system-metrics` 単一 Meter で束ねる。
+- **Rationale**: いずれも「ランタイム / ホストのリソース観測」という単一目的で、`system.*`/`process.*` の prefix で十分名前空間が分離できる。Meter を分けると `addBatchObservableCallback` の呼び出しと spec も二重になり管理コスト増。
+- **Trade-offs**: 将来「process 系のみオフにする」のような細かい制御が困難になるが、現時点で必要性なし。
+
+### Decision: Anonymization は best-effort, module-based, opt-in
+
+- **Context**: 個人情報(検索クエリ・ページパス・ユーザー名)が `http.target` 経由でトレースに残るリスクを下げたいが、auto-instrumentation の挙動を完全に制御することはできない。
+- **Selected Approach**:
+  1. `otel:anonymizeInBestEffort` が `true` のときのみ `startIncomingSpanHook` を注入。
+  2. handler は `AnonymizationModule` interface に従い、`canHandle` で対象選別 / `handle` で attribute を返す。
+  3. 4 つの handler を配列順で評価し、複数 module がマッチしたら `Object.assign` でマージ。
+- **Rationale**: opt-in にすることで導入リスクを抑え、module 化により拡張時の差分が局所化される。
+- **Trade-offs**: 配列順への暗黙依存があり、追加時に既存 handler との衝突確認が必要。
+
+### Decision: SemConv の不安定 attribute は `semconv.ts` にコピー
+
+- **Context**: `@opentelemetry/semantic-conventions/incubating` は minor リリースで破壊的変更を含む可能性があるとアナウンスされている。
+- **Selected Approach**: `service.instance.id`, `http.target` をローカル定数として保持し、ランタイムコードからは local file のみを import する。
+- **Rationale**: OpenTelemetry の[公式推奨](https://opentelemetry.io/docs/specs/semconv/non-normative/code-generation/#stability-and-versioning)に沿う。
+- **Follow-up**: 該当 attribute が stable promotion されたら、stable import に切り替えて local 定数を撤去(Revalidation Trigger)。
+
+### Decision: Metric export interval は 5 分
+
+- **Context**: メトリクス export 頻度は OTLP 帯域と receiving side の負荷、観測解像度のトレードオフ。
+- **Selected Approach**: `PeriodicExportingMetricReader` の `exportIntervalMillis` を 300000(5 分)に設定。
+- **Rationale**: GROWI のメトリクスは business カウント(users / pages)と config 情報が中心で、秒オーダーの解像度は不要。export 頻度を下げることで OTLP 帯域と receiving side の負荷を抑える。
+- **Trade-offs**: メモリ使用量の急変は最大 5 分遅れて観測される。OOM 直前検知などの用途には不十分だが、本 spec の範囲ではトレードオフを受容する。
+
+### Decision: Auto-instrumentation は pino と fs を除外
+
+- **Context**: `getNodeAutoInstrumentations()` を全有効化すると pino log と fs operation がトレース化される。
+- **Selected Approach**: `@opentelemetry/instrumentation-pino` と `@opentelemetry/instrumentation-fs` を `enabled: false` で明示的に無効化。
+- **Rationale**:
+  - **pino**: GROWI は log signal を OTel に送らない。pino instrumentation はトレースに log を相関させる目的だが、現状は使用しない。
+  - **fs**: ファイル I/O が極めて頻繁で、有効化すると span 量が膨大になる。OpenTelemetry 公式の[ガイド](https://opentelemetry.io/docs/languages/js/libraries/#registration)も無効化を推奨。
+
+### Decision: `service.instance.id` は config 値の passthrough、自動生成しない
+
+- **Context**: OTel SDK には `service.instance.id` を UUID 等で自動生成する resource detector があるが、GROWI ではどう扱うか。
+- **Selected Approach**: `otel:serviceInstanceId`(env: `OPENTELEMETRY_SERVICE_INSTANCE_ID`)を優先、フォールバックで `app:serviceInstanceId`(DB 由来)を使用。両方 undefined の場合は emit しない。
+- **Rationale**: 自動生成すると再起動ごとに ID が変わり「同じ GROWI インスタンス」の経時観測が困難になる。明示的に与えられた ID のみを passthrough することで、運用者がレプリカの境界を制御できる。
+- **Trade-offs**: ID 未指定時に emit されないため、レプリカ識別が必要なクエリは値の有無を考慮する必要がある。
+
+## Risks & Mitigations
+
+- **下流ダッシュボードの参照切れ**: 既存 Resource Attribute / Metric を将来変更した場合、receiving side のクエリが値を返さなくなる。**Mitigation**: PR 説明とリリースノートに「Removed → Replaced by」の対応表を記載する慣習を維持する。
+- **`process.constrainedMemory()` のプラットフォーム依存**: Linux cgroup v1/v2 のみサポートで、macOS/Windows では常に 0 を返す。**Mitigation**: 0 のときは `system.memory.limit` を観測しない挙動が、そのまま非対応プラットフォームの振る舞いと一致するため追加対策不要。
+- **新規メトリクスのカーディナリティ**: 観測値メトリクスは label を持たない gauge であり、追加カーディナリティ寄与はインスタンス分のみ。**Mitigation**: 設計上、観測値メトリクスには attribute を付与しないことを徹底(identity は Resource、設定値は `growi.configs` ラベル経由)。
+- **NodeSDK private アクセスの破綻**: `_resource` プロパティが SDK メジャー更新で消滅する可能性。**Mitigation**: Revalidation Trigger として SDK バージョンアップ時にチェック。public API が出たら即座に切り替え。
+- **Anonymization の網羅性不足**: 新規 API パスが追加されたとき、対応する handler を忘れると平文の URL が span に残る。**Mitigation**: 新規 API 追加時のレビューで `anonymization/handlers/` の更新有無を確認する文化を維持。`handlers/index.ts` の `anonymizationModules` 配列が単一の真実ソース。
+- **SemConv 不安定 attribute の stable promotion 漏れ**: `service.instance.id` / `http.target` が stable 化されているのに local 定数を放置すると、最新 OTLP 受信側との互換性が崩れる可能性。**Mitigation**: `@opentelemetry/semantic-conventions` メジャー / minor 更新時に Revalidation Trigger で見直す。
+
+## References
+
+- [OpenTelemetry Node.js SDK](https://open-telemetry.github.io/opentelemetry-js/)
+- [Custom Metrics Documentation](https://opentelemetry.io/docs/instrumentation/js/manual/#creating-metrics)
+- [HTTP Instrumentation Configuration](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http#configuration)
+- [Semantic Conventions for System Metrics](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md)
+- [Semantic Conventions for Process](https://opentelemetry.io/docs/specs/semconv/runtime-environment/process/)
+- [Resource Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md)
+- [SemConv Stability and Versioning](https://opentelemetry.io/docs/specs/semconv/non-normative/code-generation/#stability-and-versioning) — incubating attribute のローカルコピー推奨。
+- [Node.js process.constrainedMemory()](https://nodejs.org/api/process.html#processconstrainedmemory) — cgroup ベースのメモリ上限取得 API。
+- [Node.js v8.getHeapStatistics()](https://nodejs.org/api/v8.html#v8getheapstatistics) — V8 ヒープ統計取得 API。
+- [OpenTelemetry — disabling instrumentations](https://opentelemetry.io/docs/languages/js/libraries/#registration) — fs instrumentation の無効化推奨。
+- 既存実装: `apps/app/src/features/opentelemetry/server/custom-metrics/application-metrics.ts` — ObservableGauge + addBatchObservableCallback のリファレンス実装。

+ 5 - 4
.kiro/specs/otel-attributes-cleanup/spec.json → .kiro/specs/opentelemetry/spec.json

@@ -1,9 +1,9 @@
 {
-  "feature_name": "otel-attributes-cleanup",
+  "feature_name": "opentelemetry",
   "created_at": "2026-05-21T00:00:00.000Z",
-  "updated_at": "2026-05-21T03:30:00.000Z",
+  "updated_at": "2026-05-21T13:20:00.000Z",
   "language": "ja",
-  "phase": "tasks-generated",
+  "phase": "implementation-complete",
   "approvals": {
     "requirements": {
       "generated": true,
@@ -18,5 +18,6 @@
       "approved": true
     }
   },
-  "ready_for_implementation": true
+  "ready_for_implementation": true,
+  "cleaned_up_at": "2026-05-21T13:20:00.000Z"
 }

+ 29 - 0
.kiro/specs/opentelemetry/tasks.md

@@ -0,0 +1,29 @@
+# Implementation Plan
+
+本 spec は `features/opentelemetry/` の **大局的なメンテナンス spec** であり、新規実装タスクを抱えないドキュメント spec として扱う。実装は既に完了しており、本ファイルは「将来の拡張時に踏むテンプレート」と「Revalidation の手順」を記録する。
+
+## Implementation Notes
+
+### 拡張時のテンプレート参照
+
+新規 Custom Metric / Anonymization Handler の追加手順は [design.md](./design.md) の **File Structure Plan → Extension Templates** を参照する。テンプレートに沿って実装することで、レビューでの差分が局所化され、本 spec の Boundary Commitments を逸脱しない。
+
+### Revalidation 必要時の対応フロー
+
+[design.md](./design.md) の **Boundary Commitments → Revalidation Triggers** に列挙された条件のいずれかが発生したら、以下を順次実施する:
+
+1. 該当する Boundary Commitments セクションを読み返し、変更が境界内で完結するかを評価。
+2. 境界をまたぐ場合は新規 spec として切り出すか、本 spec の Revalidation Triggers と Design Decisions を更新する。
+3. 受信側ダッシュボード / クエリへの影響がある場合は、PR 説明 / リリースノートに「Removed → Replaced by」の対応表を添える。
+
+### 将来の取り扱い候補(Out of Boundary の再評価候補)
+
+以下は本 spec の Out of Boundary に該当するが、将来の要望次第で別 spec として切り出す候補:
+
+- OpenTelemetry Log Signal の利用開始(pino との統合)。
+- CPU / network / GC / event-loop lag メトリクスの追加。
+- `deployment.environment.name`(OTel 標準)への対応。
+- ブラウザ telemetry(Web SDK)の導入。
+- `@opentelemetry/host-metrics` への置き換え(要件 5 を満たせる版がリリースされたら)。
+
+これらは現時点では要件として上がっていないため、追加要望時に新規 spec を起こす。

+ 0 - 91
.kiro/specs/otel-attributes-cleanup/brief.md

@@ -1,91 +0,0 @@
-# Brief: otel-attributes-cleanup
-
-## Problem
-The OpenTelemetry infrastructure admin asked to remove `os.totalmem` from custom resource attributes, prompting a full audit of GROWI's custom resource attributes and metrics. The current setup mixes three concerns under "resource attributes":
-
-1. **Host identity** (`os.type`, `os.platform`, `os.arch`) — legitimate identity, must stay.
-2. **A measurement** (`os.totalmem`) — bytes value that gets stamped on every metric/trace/log, inflating payloads and misleading users in container environments where `os.totalmem()` returns the *host's* RAM, not the cgroup memory limit.
-3. **Subsystem configuration** (`growi.attachment.type`) — a config value (aws / gcs / gridfs / ...) that is conceptually identical to `wiki_type` and `external_auth_types`, which already live as labels on the `growi.configs` info gauge.
-
-In addition, GROWI is primarily operated as a container workload, so the missing pieces are: cgroup-aware memory limit, process RSS, and V8 heap stats — none of which exist today.
-
-## Current State
-- `apps/app/src/features/opentelemetry/server/custom-resource-attributes/`
-  - `os-resource-attributes.ts` exports `os.type` / `os.platform` / `os.arch` / `os.totalmem`.
-  - `application-resource-attributes.ts` exports `growi.service.type` / `growi.deployment.type` / `growi.attachment.type`.
-- `apps/app/src/features/opentelemetry/server/custom-metrics/`
-  - `application-metrics.ts` emits `growi.configs` (Prometheus info pattern, value = 1) with labels `site_url`, `site_url_hashed`, `wiki_type`, `external_auth_types`.
-  - `page-counts-metrics.ts`, `user-counts-metrics.ts` emit gauges.
-- No system / process memory metrics exist.
-- `@opentelemetry/host-metrics` is **not** installed.
-
-## Desired Outcome
-- Resource attributes contain only identity-class data; no measurements, no subsystem config.
-- Memory information (both static "limit" and live "usage") is observable as proper metrics, with the cgroup limit and the host total emitted as *separate* metrics so operators can tell containerized vs bare-metal resource constraints apart.
-- Subsystem configuration (`attachment.type`) is consolidated into the existing `growi.configs` info gauge labels alongside `wiki_type` / `external_auth_types`.
-- Dashboards & alerts on the otel-infra side can migrate from the removed resource attributes to the new metric names with minimal churn.
-
-## Approach
-**Reorganize, do not rewrite.** Three coordinated changes:
-
-1. **Remove from resource attributes**
-   - `os.totalmem` (move to a metric).
-   - `growi.attachment.type` (move to a label on the existing info gauge).
-
-2. **Add a new metrics module: `custom-metrics/system-metrics.ts`**, using only Node.js standard modules (`node:os`, `node:v8`, `node:process`) — no new package dependency.
-   - `system.memory.limit` — `process.constrainedMemory()` (cgroup v1/v2 limit, Node 20.12+). Skip observation when the value is `0`/`undefined` (i.e. unconstrained).
-   - `system.host.memory.total` — `os.totalmem()` (physical host memory, always observable).
-   - `process.memory.usage` — `process.memoryUsage().rss`.
-   - `process.runtime.v8.heap.used` — `v8.getHeapStatistics().used_heap_size`.
-   - `process.runtime.v8.heap.total` — `v8.getHeapStatistics().total_heap_size`.
-   - `process.runtime.v8.heap.external` — `process.memoryUsage().external`.
-   - All as `ObservableGauge` with unit `By`.
-
-3. **Extend `growi.configs` info gauge** in `application-metrics.ts` with a new `attachment_type` label (matching the existing snake_case naming of sibling labels).
-
-### Why custom ObservableGauges, not `@opentelemetry/host-metrics`
-The package (latest 0.38.3) does not emit `system.memory.limit` and does not use `process.constrainedMemory()` — it reads `os.totalmem()`/`os.freemem()` directly, which defeats the container-awareness goal. It also lacks V8 heap stats and would require us to hand-write the missing metrics anyway, while pulling in `systeminformation` and emitting network/CPU metrics we did not ask for. ~50 lines of custom code is cleaner and fully controllable.
-
-## Scope
-- **In**:
-  - Removing `os.totalmem` from `os-resource-attributes.ts` (and updating its spec).
-  - Removing `growi.attachment.type` from `application-resource-attributes.ts` (and updating its spec).
-  - Adding `attachment_type` label to the `growi.configs` info gauge in `application-metrics.ts` (and updating its spec).
-  - Creating `custom-metrics/system-metrics.ts` with the 6 metrics listed above, plus a spec file.
-  - Wiring `addSystemMetrics()` into `custom-metrics/index.ts` `setupCustomMetrics()`.
-  - Brief operator-facing note describing the rename mapping (resource attr removed → new metric/label) so the otel-infra admin can update dashboards.
-- **Out**:
-  - Any other custom resource attribute that already passes the identity test (`os.type/platform/arch`, `growi.service.type`, `growi.deployment.type` all stay as-is).
-  - CPU metrics, event loop lag, GC metrics — not requested; can be added later if needed.
-  - Network metrics.
-  - Touching the anonymization layer (`http.target` etc.) — separate concern.
-  - Span attributes — only resource attributes are reorganized in this spec.
-  - Adopting `@opentelemetry/host-metrics` package.
-
-## Boundary Candidates
-- Resource attribute pruning (deletions only) — touches 2 files in `custom-resource-attributes/`.
-- Metric additions — new file in `custom-metrics/` + wire-in.
-- Info-gauge label extension — one-line change in `custom-metrics/application-metrics.ts`.
-
-These three slices can be implemented and reviewed independently, but ship as one release for a single observable contract change.
-
-## Out of Boundary
-- Renaming or restructuring the existing `growi.*` metrics (`growi.pages.total`, `growi.users.total`, `growi.users.active`).
-- Migrating `growi.deployment.type` to the OTel-standard `deployment.environment.name` (decided to keep as `growi.deployment.type` — different semantic from "environment").
-- Touching `node-sdk-configuration.ts` core service identity attributes (`service.name`, `service.version`, `service.instance.id`).
-- Anonymization (`http.target`).
-- Adopting `@opentelemetry/host-metrics`.
-
-## Upstream / Downstream
-- **Upstream**: `growiInfoService.getGrowiInfo({ includeAttachmentInfo: true })` continues to be the source of `attachment.type`. No service-layer change required.
-- **Downstream**: External OpenTelemetry collector / Prometheus / Grafana dashboards operated by the otel-infra admin. They will need to update queries that previously read `os.totalmem` and `growi.attachment.type` as resource attributes — coordination via a "what changed" note in the PR description.
-
-## Existing Spec Touchpoints
-- **Extends**: None — there is no prior `.kiro/specs/` entry for OpenTelemetry. This is a standalone refactor spec for the existing `features/opentelemetry/` module.
-- **Adjacent**: `features/opentelemetry/server/anonymization/` is unrelated and untouched.
-
-## Constraints
-- Node.js runtime must be ≥ 20.12 for `process.constrainedMemory()` (verify against current `apps/app/package.json` `engines` field; the existing `@opentelemetry/host-metrics` peer would require ≥ 18.19 / 20.6, so 20.12 is well within the GROWI baseline).
-- Must not introduce new runtime dependencies (in line with the rule that any package appearing in `apps/app/.next/node_modules/` after build needs `dependencies` classification — adding none means zero classification risk).
-- Backwards-compatible at the OTLP wire level: only additions to metrics, only removals from resource attributes. Communicate the resource-attribute removals explicitly to the otel-infra admin.
-- All new metric names follow OTel semconv where a stable name exists (`system.memory.*`, `process.memory.usage`). Where no standard exists, use `process.runtime.v8.*` to align with the existing Node.js community conventions.

+ 0 - 411
.kiro/specs/otel-attributes-cleanup/design.md

@@ -1,411 +0,0 @@
-# Technical Design — otel-attributes-cleanup
-
-## Overview
-
-**Purpose**: GROWI OpenTelemetry 統合における Resource Attribute / Metric の責務を再分類し、コンテナ運用に必須のメモリ系メトリクスを追加する。
-
-**Users**: OpenTelemetry 受信側インフラ管理者(GROWI からの telemetry を Prometheus / Grafana 等に取り込む運用者)。
-
-**Impact**: Resource Attribute から `os.totalmem` と `growi.attachment.type` を削除する破壊的変更を伴い、その代わりに `growi.configs` info gauge の新ラベル 1 個と、新規メトリクス 6 個(`system.*` 2 個、`process.memory.usage` 1 個、`process.runtime.v8.heap.*` 3 個)を追加する。テレメトリ全体の wire 形式は OTLP のままで変更なし。
-
-### Goals
-
-- Resource Attribute を identity 専用に整理し、measurement / 設定値を排除する。
-- コンテナ運用環境で cgroup memory limit とホスト物理メモリ総量を別メトリクスとして観測可能にする。
-- プロセス RSS と V8 ヒープ統計を継続観測可能にする。
-- 既存のカスタムメトリクスモジュール構造(1 モジュール = 1 ファイル = 1 setup 関数)に整合させ、レビュー差分を最小化する。
-
-### Non-Goals
-
-- 既存メトリクス(`growi.configs`, `growi.users.*`, `growi.pages.*`)の名称変更や再構成。
-- `growi.deployment.type` の OTel semconv (`deployment.environment.name`) への移行。
-- CPU / ネットワーク / GC / event loop lag 等のメトリクス追加。
-- HTTP anonymization layer (`http.target` 等の span attribute) への変更。
-- 外部パッケージ(`@opentelemetry/host-metrics` 等)の導入。
-
-## Boundary Commitments
-
-### This Spec Owns
-
-- `os-resource-attributes.ts` から `os.totalmem` の出力を削除する責務。
-- `application-resource-attributes.ts` から `growi.attachment.type` の出力を削除する責務。
-- `application-metrics.ts` の `growi.configs` gauge に `attachment_type` ラベルを追加する責務。
-- 新規モジュール `system-metrics.ts` の追加と、その 6 メトリクス(`system.memory.limit`, `system.host.memory.total`, `process.memory.usage`, `process.runtime.v8.heap.used`, `process.runtime.v8.heap.total`, `process.runtime.v8.heap.external`)の責務。
-- `custom-metrics/index.ts` の `setupCustomMetrics()` への `addSystemMetrics()` の追加。
-- 上記すべてに対応する spec.ts ファイルの追加・更新。
-- リリース時の運用者向け移行マッピング(PR 説明 / リリースノート)。
-
-### Out of Boundary
-
-- `node-sdk-configuration.ts` 内の core service identity 属性(`service.name`, `service.version`, `service.instance.id`)。
-- 他のカスタムメトリクスモジュール(`page-counts-metrics.ts`, `user-counts-metrics.ts`)の変更。
-- HTTP anonymization(`anonymization/` 配下)。
-- `growi.deployment.type` の renaming / 移行。
-- CPU / network / GC / event-loop メトリクスの追加(将来要望時に別 spec で扱う)。
-- `growi.configs` の既存ラベル(`site_url`, `site_url_hashed`, `wiki_type`, `external_auth_types`)の名称・値・付与条件。
-
-### Allowed Dependencies
-
-- Node.js 標準モジュール: `node:os`, `node:v8`, `node:process`。
-- 既存 OpenTelemetry パッケージ: `@opentelemetry/api`(Meter / ObservableGauge / diag)。
-- 既存サービス: `~/server/service/growi-info`(`growiInfoService.getGrowiInfo({ includeAttachmentInfo: true })`)。
-- 既存ユーティリティ: `~/utils/logger`。
-- **新規 npm 依存の追加は不可。**
-
-### Revalidation Triggers
-
-- `growiInfo.additionalInfo.attachmentType` の型・値域変更 → `growi.configs` のラベル付与ロジック再確認。
-- `apps/app/package.json` の `engines.node` が v20.12 未満にダウングレード → `process.constrainedMemory()` 可用性の再確認(現状 `^24` なのでリスクなし)。
-- `@opentelemetry/api` のメジャー更新(特に Meter / ObservableGauge API) → 全カスタムメトリクスの呼び出し方再確認。
-- 受信側ダッシュボードのクエリ更新が未完了の状態でのリリース → ロールアウト順序の再調整。
-
-## Architecture
-
-### Existing Architecture Analysis
-
-`features/opentelemetry/server/` は以下のような構造で確立している:
-
-- `node-sdk-configuration.ts` が SDK 初期化と core Resource Attribute(service.*)を組み立てる。
-- `node-sdk-resource.ts` / `generateAdditionalResourceAttributes()` が DB 初期化後に呼ばれ、`custom-resource-attributes/` から identity を補完する。
-- `custom-metrics/setupCustomMetrics()` が起動時に各カスタムメトリクスモジュール(application / user-counts / page-counts)を順次登録する。
-- 各カスタムメトリクスモジュールは `addXxxMetrics(): void` を export し、`metrics.getMeter('growi-<scope>-metrics', '1.0.0')` で Meter を取得 → `createObservableGauge` → `addBatchObservableCallback` の三段構成で実装される。
-
-この構造をそのまま踏襲し、Resource Attribute 側はファイル内のキー削除のみ、Metric 側は新規 1 ファイル追加と既存 2 ファイルへの局所修正で完結させる。
-
-### Architecture Pattern & Boundary Map
-
-```mermaid
-graph LR
-    subgraph custom_resource_attributes
-        OsRA[os-resource-attributes.ts]
-        AppRA[application-resource-attributes.ts]
-    end
-    subgraph custom_metrics
-        AppMet[application-metrics.ts]
-        UserMet[user-counts-metrics.ts]
-        PageMet[page-counts-metrics.ts]
-        SysMet[system-metrics.ts NEW]
-        Index[index.ts setupCustomMetrics]
-    end
-    GrowiInfo[growiInfoService]
-    NodeOs[node:os]
-    NodeV8[node:v8]
-    NodeProc[node:process]
-    Otel[OpenTelemetry SDK]
-
-    OsRA --> Otel
-    AppRA --> GrowiInfo
-    AppRA --> Otel
-    AppMet --> GrowiInfo
-    AppMet --> Otel
-    UserMet --> Otel
-    PageMet --> Otel
-    SysMet --> NodeOs
-    SysMet --> NodeV8
-    SysMet --> NodeProc
-    SysMet --> Otel
-    Index --> AppMet
-    Index --> UserMet
-    Index --> PageMet
-    Index --> SysMet
-```
-
-**Key Decisions**:
-- 新規モジュール `system-metrics.ts` は `growiInfoService` に依存せず、Node.js stdlib のみを参照する(DB 初期化前でも動作可能だが、`setupCustomMetrics()` 経由で起動するため実行タイミングは他と同一)。
-- 既存 4 モジュールに対する変更はすべて「ファイル内の追加・削除」で完結し、新たな相互依存は導入しない。
-
-### Technology Stack
-
-| Layer | Choice / Version | Role in Feature | Notes |
-|-------|------------------|-----------------|-------|
-| Runtime | Node.js `^24` | `process.constrainedMemory()`, `v8.getHeapStatistics()` 等 stdlib API を利用 | `engines` 既定値、変更なし |
-| Telemetry SDK | `@opentelemetry/api ^1.9.0`, `@opentelemetry/sdk-metrics ^2.0.1` | Meter / ObservableGauge / diag | 既存導入済み、変更なし |
-| Test | Vitest(既存設定) | `vi.mock('node:os')`, `vi.spyOn(process, 'constrainedMemory')`, `mock<Meter>()` パターン | 既存導入済み |
-
-新規 npm 依存の追加なし。
-
-## File Structure Plan
-
-### Directory Structure
-
-```
-apps/app/src/features/opentelemetry/server/
-├── custom-resource-attributes/
-│   ├── os-resource-attributes.ts          # 修正: os.totalmem を削除
-│   ├── os-resource-attributes.spec.ts     # 修正: totalmem 関連アサーション削除
-│   ├── application-resource-attributes.ts # 修正: growi.attachment.type を削除
-│   └── application-resource-attributes.spec.ts # 修正: attachment.type 関連アサーション削除
-└── custom-metrics/
-    ├── application-metrics.ts             # 修正: attachment_type ラベルを追加
-    ├── application-metrics.spec.ts        # 修正: attachment_type のテストケース追加
-    ├── system-metrics.ts                  # 新規: 6 メトリクスを emit
-    ├── system-metrics.spec.ts             # 新規: テスト
-    └── index.ts                           # 修正: addSystemMetrics の export と setupCustomMetrics への登録
-```
-
-### Modified Files
-
-- `custom-resource-attributes/os-resource-attributes.ts` — `os.totalmem` を `osInfo` と返り値 attributes から削除。型と関数シグネチャは維持。
-- `custom-resource-attributes/os-resource-attributes.spec.ts` — `vi.mock('node:os')` から `totalmem` のモックを除去、3 テストの期待値から `os.totalmem` キーを削除。
-- `custom-resource-attributes/application-resource-attributes.ts` — 返り値 attributes から `growi.attachment.type` 行を削除。同時に `getGrowiInfo` 呼び出しから `includeAttachmentInfo: true` フラグも除去(このファイルからは `attachmentType` を参照しなくなるため)。
-- `custom-resource-attributes/application-resource-attributes.spec.ts` — `growi.attachment.type` 関連アサーションを削除。
-- `custom-metrics/application-metrics.ts` — `result.observe(growiInfoGauge, 1, { ... })` のラベルオブジェクトに `attachment_type: growiInfo.additionalInfo?.attachmentType ?? ''` を追加するのみ(`getGrowiInfo({ includeAttachmentInfo: true })` 呼び出しは既に存在するため変更不要)。
-- `custom-metrics/application-metrics.spec.ts` — 期待ラベルに `attachment_type: <value>` を追加するテストケースを追加。空文字フォールバックのケースも追加。
-- `custom-metrics/index.ts` — `export { addSystemMetrics } from './system-metrics';` を追加し、`setupCustomMetrics()` 内で `addSystemMetrics()` を呼び出す。
-
-### New Files
-
-- `custom-metrics/system-metrics.ts` — 1 ファイルで 6 メトリクスを emit する `addSystemMetrics(): void` を export。
-- `custom-metrics/system-metrics.spec.ts` — `node:os` / `node:v8` / `process.constrainedMemory` をモックして、6 メトリクスそれぞれの観測 / `system.memory.limit` の cgroup 未設定時スキップ / エラー時の挙動を検証。
-
-## Requirements Traceability
-
-| Requirement | Summary | Components | Interfaces | Flows |
-|-------------|---------|------------|------------|-------|
-| 1.1 | `os.totalmem` を Resource Attribute に含めない | OsResourceAttributes | `getOsResourceAttributes()` 戻り値からキー削除 | — |
-| 1.2 | `growi.attachment.type` を Resource Attribute に含めない | ApplicationResourceAttributes | `getApplicationResourceAttributes()` 戻り値からキー削除 | — |
-| 1.3 | 既存 identity 系 attribute を変更しない | OsResourceAttributes, ApplicationResourceAttributes, node-sdk-configuration | 既存戻り値の他キー維持 | — |
-| 2.1 | `growi.configs` に `attachment_type` ラベル付与 | ApplicationMetrics | `result.observe(growiInfoGauge, 1, { ..., attachment_type })` | — |
-| 2.2 | 既存ラベルを変更しない | ApplicationMetrics | observe 呼び出しの他キー維持 | — |
-| 2.3 | 取得不能時は空文字フォールバック | ApplicationMetrics | `attachmentType ?? ''` | — |
-| 3.1 | cgroup limit 設定時 `system.memory.limit` を観測 | SystemMetrics | `process.constrainedMemory() > 0` 時に `result.observe(memoryLimitGauge, value)` | — |
-| 3.2 | cgroup 未設定時はスキップ | SystemMetrics | 条件分岐で observe をスキップ | — |
-| 3.3 | `system.host.memory.total` を常に観測 | SystemMetrics | `result.observe(hostMemoryTotalGauge, os.totalmem())` | — |
-| 4.1 | `process.memory.usage` を観測 | SystemMetrics | `result.observe(processMemoryUsageGauge, process.memoryUsage().rss)` | — |
-| 4.2 | `process.runtime.v8.heap.used` を観測 | SystemMetrics | `result.observe(v8HeapUsedGauge, v8.getHeapStatistics().used_heap_size)` | — |
-| 4.3 | `process.runtime.v8.heap.total` を観測 | SystemMetrics | `result.observe(v8HeapTotalGauge, v8.getHeapStatistics().total_heap_size)` | — |
-| 4.4 | `process.runtime.v8.heap.external` を観測 | SystemMetrics | `result.observe(v8HeapExternalGauge, process.memoryUsage().external)` | — |
-| 5.1 | setup 時に新規モジュールを起動 | CustomMetricsIndex | `setupCustomMetrics()` 内で `addSystemMetrics()` を呼ぶ | — |
-| 5.2 | コールバック内例外を吸収しログ | SystemMetrics | try/catch + `diag.createComponentLogger(...).error(...)` | — |
-| 6.1 | 残存属性 / メトリクスを変更しない | 全モジュール | 削除以外の差分なし(既存 export / シグネチャ維持) | — |
-| 6.2 | 移行マッピングを運用者に明示 | (PR / リリースノート) | 設計成果物外(ドキュメンテーション) | — |
-
-## Components and Interfaces
-
-| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
-|-----------|--------------|--------|--------------|------------------|-----------|
-| OsResourceAttributes | Resource Attribute | OS identity 属性を提供 | 1.1, 1.3 | `node:os` (P0) | Service |
-| ApplicationResourceAttributes | Resource Attribute | GROWI service identity を提供 | 1.2, 1.3 | `growiInfoService` (P0) | Service |
-| ApplicationMetrics | Metric | GROWI 設定情報を info gauge で出力 | 2.1, 2.2, 2.3 | `growiInfoService` (P0), `@opentelemetry/api` (P0) | Service |
-| SystemMetrics(新規) | Metric | コンテナ / プロセスメモリの観測値を出力 | 3.1, 3.2, 3.3, 4.1, 4.2, 4.3, 4.4, 5.2 | `node:os`/`node:v8`/`node:process` (P0), `@opentelemetry/api` (P0) | Service |
-| CustomMetricsIndex | Composition | カスタムメトリクス起動の合成点 | 5.1 | 各メトリクスモジュール (P0) | Service |
-
-### Custom Resource Attributes Layer
-
-#### OsResourceAttributes
-
-| Field | Detail |
-|-------|--------|
-| Intent | OS identity を OTel Resource Attribute として返す |
-| Requirements | 1.1, 1.3 |
-
-**Responsibilities & Constraints**
-- `os.type`, `os.platform`, `os.arch` を返す。
-- `os.totalmem` は **返さない**。
-
-**Dependencies**
-- External: `node:os` — `type()`, `platform()`, `arch()` のみ呼ぶ(`totalmem()` は呼ばない) (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
-```typescript
-export function getOsResourceAttributes(): Attributes;
-// 戻り値: { 'os.type': string, 'os.platform': string, 'os.arch': string }
-```
-
-**Implementation Notes**
-- 整合性: 既存関数シグネチャは維持し、戻り値オブジェクトからキーを除くだけにする。`logger.info` の "Collecting OS resource attributes" メッセージはそのまま。
-
-#### ApplicationResourceAttributes
-
-| Field | Detail |
-|-------|--------|
-| Intent | GROWI service identity を OTel Resource Attribute として返す |
-| Requirements | 1.2, 1.3 |
-
-**Responsibilities & Constraints**
-- `growi.service.type`, `growi.deployment.type` を返す。
-- `growi.attachment.type` は **返さない**。
-
-**Dependencies**
-- Inbound: `node-sdk-configuration.ts` の `generateAdditionalResourceAttributes` (P0)
-- Outbound: `growiInfoService.getGrowiInfo()` (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
-```typescript
-export async function getApplicationResourceAttributes(): Promise<Attributes>;
-// 戻り値: { 'growi.service.type': string, 'growi.deployment.type': string }
-```
-
-**Implementation Notes**
-- 整合性: `getGrowiInfo()` 呼び出し時の `includeAttachmentInfo: true` フラグはこのモジュールでは不要となるため除去する。`attachmentType` の値が必要なのは `application-metrics.ts` 側に移行する。
-- 既存のエラーハンドリング(try/catch → 空 Attributes 返却)は維持。
-
-### Custom Metrics Layer
-
-#### ApplicationMetrics
-
-| Field | Detail |
-|-------|--------|
-| Intent | GROWI 設定情報を info gauge `growi.configs` のラベルに集約 |
-| Requirements | 2.1, 2.2, 2.3 |
-
-**Responsibilities & Constraints**
-- `growi.configs` ObservableGauge(値は常に 1)に既存 4 ラベル + `attachment_type` を付与。
-- ラベル命名は snake_case で既存と整合。
-- `attachmentType` 未取得時は空文字 `''` フォールバック。
-
-**Dependencies**
-- Outbound: `growiInfoService.getGrowiInfo({ includeAttachmentInfo: true })` (P0)
-- External: `@opentelemetry/api` Meter / ObservableGauge / diag (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
-(既存と同じ。シグネチャ変更なし)
-```typescript
-export function addApplicationMetrics(): void;
-```
-
-##### Label Schema 変更後の `growi.configs` ラベル
-| Label | Source | Notes |
-|-------|--------|-------|
-| `site_url` | `isAppSiteUrlHashed ? '[hashed]' : growiInfo.appSiteUrl` | 既存維持 |
-| `site_url_hashed` | `isAppSiteUrlHashed ? hash(appSiteUrl) : undefined` | 既存維持 |
-| `wiki_type` | `growiInfo.wikiType` | 既存維持 |
-| `external_auth_types` | `additionalInfo?.activeExternalAccountTypes?.join(',') \|\| ''` | 既存維持 |
-| `attachment_type` | `additionalInfo?.attachmentType ?? ''` | **新規追加** |
-
-**Implementation Notes**
-- 整合性: `result.observe(growiInfoGauge, 1, { ... })` のオブジェクトリテラルに 1 行追加するのみ。それ以外の制御フローは無変更。
-
-#### SystemMetrics(新規)
-
-| Field | Detail |
-|-------|--------|
-| Intent | コンテナ / ホスト / プロセス / V8 ヒープのメモリ系統計を ObservableGauge で出力 |
-| Requirements | 3.1, 3.2, 3.3, 4.1, 4.2, 4.3, 4.4, 5.2 |
-
-**Responsibilities & Constraints**
-- 単一 Meter `growi-system-metrics`(version `'1.0.0'`)で 6 つの ObservableGauge を作成する。
-- すべての gauge は単位 `By`(bytes)。
-- 1 つの `addBatchObservableCallback` で全 gauge を観測(コールバック 1 回で 6 観測)。
-- `process.constrainedMemory()` が `> 0` の場合のみ `system.memory.limit` を観測。`0` または `undefined` のときはこの 1 メトリクスのみスキップし、他 5 メトリクスは観測する。
-- コールバック内で発生した例外は try/catch で吸収し `diag.createComponentLogger` でエラーログを出す。
-
-**Dependencies**
-- External: `node:os` (`totalmem()`), `node:v8` (`getHeapStatistics()`), `node:process` (`constrainedMemory()`, `memoryUsage()`) (P0)
-- External: `@opentelemetry/api` Meter / ObservableGauge / BatchObservableCallback / diag (P0)
-
-**Contracts**: Service [x]
-
-##### Service Interface
-```typescript
-export function addSystemMetrics(): void;
-```
-- Preconditions: OpenTelemetry SDK が初期化済み(`metrics.getMeter` が有効な Meter を返す)。
-- Postconditions: 1 Meter / 6 ObservableGauge / 1 BatchObservableCallback が登録される。
-- Invariants: 各 collection cycle で `system.memory.limit` 以外は必ず観測される。
-
-##### Metric Schema
-| Metric Name | Unit | Source | Skip Condition |
-|-------------|------|--------|----------------|
-| `system.memory.limit` | `By` | `process.constrainedMemory()` | 値が `0` または falsy |
-| `system.host.memory.total` | `By` | `os.totalmem()` | — |
-| `process.memory.usage` | `By` | `process.memoryUsage().rss` | — |
-| `process.runtime.v8.heap.used` | `By` | `v8.getHeapStatistics().used_heap_size` | — |
-| `process.runtime.v8.heap.total` | `By` | `v8.getHeapStatistics().total_heap_size` | — |
-| `process.runtime.v8.heap.external` | `By` | `process.memoryUsage().external` | — |
-
-**Implementation Notes**
-- 整合性: 既存 `application-metrics.ts` のスタイルを踏襲(`loggerFactory` / `loggerDiag` / `metrics.getMeter` / `addBatchObservableCallback`)。
-- 観測の効率: `process.memoryUsage()` と `v8.getHeapStatistics()` はそれぞれ 1 コールバック内で 1 回ずつ呼び出し、結果をローカル変数に保持してから 6 つの gauge を観測する(API 呼び出しの重複を避ける)。
-- リスク: なし(追加 dep ゼロ、stdlib 呼び出しのみ、副作用なし)。
-
-#### CustomMetricsIndex
-
-| Field | Detail |
-|-------|--------|
-| Intent | カスタムメトリクスモジュール群を順次起動 |
-| Requirements | 5.1 |
-
-**Responsibilities & Constraints**
-- `setupCustomMetrics()` 内で既存 3 関数に加え `addSystemMetrics()` を呼び出す。
-- 既存の dynamic import パターン(`await import('./xxx')`)を踏襲する。
-
-**Dependencies**
-- Inbound: SDK 起動シーケンス(`generateAdditionalResourceAttributes` の後段)
-- Outbound: `addApplicationMetrics`, `addUserCountsMetrics`, `addPageCountsMetrics`, `addSystemMetrics`
-
-**Contracts**: Service [x]
-
-##### Service Interface
-(既存シグネチャ維持、内部で 1 行追加)
-```typescript
-export const setupCustomMetrics = async (): Promise<void>;
-```
-
-## Error Handling
-
-### Error Strategy
-
-`SystemMetrics` のコールバック内で例外が発生した場合、既存の `application-metrics.ts` と同じく以下を行う:
-
-1. `try { ... } catch (error) { loggerDiag.error(...) }` で例外を吸収。
-2. 例外発生時、その collection cycle では何も観測しない(残りの gauge も不確実な値で観測しない)。
-3. 次回 collection cycle で再試行(ObservableGauge の自然な振る舞い)。
-
-### Error Categories and Responses
-
-| Category | 例 | 振る舞い |
-|----------|-----|---------|
-| stdlib 呼び出し失敗 | `v8.getHeapStatistics()` が想定外の例外を throw(実際には発生しない) | `loggerDiag.error` してスキップ |
-| `process.constrainedMemory()` の戻り値が異常 | 0 または undefined | `system.memory.limit` のみスキップ、他は観測継続 |
-| Meter 取得失敗 | SDK 未初期化等 | `addSystemMetrics()` 起動時に例外伝播。`setupCustomMetrics()` の呼び出し元責任。 |
-
-### Monitoring
-
-- `diag.createComponentLogger({ namespace: 'growi:custom-metrics:system' })` で例外を OTel diag ロガーに記録(既存パターンと整合)。
-- `loggerFactory('growi:opentelemetry:custom-metrics:system')` でアプリケーションロガーにも info ログを出力(起動完了時メッセージ)。
-
-## Testing Strategy
-
-### Unit Tests
-
-新規・更新ファイルごとに以下のテスト項目を含める:
-
-- **`os-resource-attributes.spec.ts`(更新)**:
-  - `os.totalmem` キーが戻り値オブジェクトに含まれないこと(`expect(result).not.toHaveProperty('os.totalmem')`)。
-  - `os.type` / `os.platform` / `os.arch` が引き続き含まれること。
-- **`application-resource-attributes.spec.ts`(更新)**:
-  - `growi.attachment.type` キーが戻り値オブジェクトに含まれないこと。
-  - `growi.service.type` / `growi.deployment.type` が引き続き含まれること。
-- **`application-metrics.spec.ts`(更新)**:
-  - `attachment_type` ラベルが期待値(`'aws'`, `'gcs'` 等)で付与されること。
-  - `additionalInfo` が undefined のとき `attachment_type: ''`(空文字)が付与されること。
-  - 既存の 4 ラベルが引き続き正しく付与されること。
-- **`system-metrics.spec.ts`(新規)**:
-  - Meter が `'growi-system-metrics', '1.0.0'` で取得されること。
-  - 6 個の `createObservableGauge` が正しい名称・unit `By` で作成されること。
-  - `process.constrainedMemory()` が正の値を返すとき、`system.memory.limit` を当該値で観測すること(要件 3.1)。
-  - `process.constrainedMemory()` が `0` を返すとき、`system.memory.limit` を観測しないこと(要件 3.2)。
-  - 他 5 メトリクスは `process.constrainedMemory()` の戻り値によらず常に観測されること。
-  - `process.memoryUsage()` の `rss` / `external` を `process.memory.usage` / `process.runtime.v8.heap.external` に正しくマップすること。
-  - `v8.getHeapStatistics()` の `used_heap_size` / `total_heap_size` を `process.runtime.v8.heap.used` / `process.runtime.v8.heap.total` に正しくマップすること。
-  - コールバック内例外時に `loggerDiag.error` が呼ばれ、`observe` が呼ばれないこと(要件 5.2)。
-
-### Integration Tests
-
-新規導入なし。本リファクタリングは module-local な変更のみで、cross-component の振る舞いを変更しない。`setupCustomMetrics()` 経由の起動順序の妥当性は unit テスト(index.ts 直接の spec は既存にないため、必要に応じて追加検討)と `node-sdk.spec.ts` の既存テスト範囲で担保する。
-
-### E2E / Manual Verification
-
-実環境(開発 devcontainer or staging container)で以下を確認する:
-
-- OTLP collector 側に削除済み Resource Attribute(`os.totalmem`, `growi.attachment.type`)が **届かない** こと。
-- `growi.configs` に `attachment_type` ラベルが **付与される** こと。
-- 6 つの新規メトリクスが期待される名前と単位で **届く** こと。
-- Docker container で `--memory=512m` を指定した場合に `system.memory.limit` が約 `536870912` を出力し、未指定時は出力されないこと(要件 3.1 / 3.2 の挙動確認)。

+ 0 - 87
.kiro/specs/otel-attributes-cleanup/requirements.md

@@ -1,87 +0,0 @@
-# Requirements Document
-
-## Introduction
-
-GROWI の OpenTelemetry 統合における **カスタム Resource Attribute / Metric の責務分離を整理する** リファクタリング。現状、Resource Attribute には (1) ホスト/サービス識別子、(2) 数値の測定値である `os.totalmem`、(3) サブシステム設定値である `growi.attachment.type` の 3 種類が混在しており、Resource は本来「テレメトリ発生元エンティティの identity」を表現するべきであるという OpenTelemetry の設計意図と合致していない。
-
-加えて、GROWI の主要な運用形態はコンテナ(Docker / Kubernetes)であるにもかかわらず、cgroup memory limit やプロセス RSS、V8 ヒープ統計などコンテナ運用に必須のメモリ系メトリクスが一切収集されていない。Resource Attribute の `os.totalmem` はホスト物理メモリを返すため、コンテナ運用ではむしろ誤解を招く位置にある。
-
-本仕様では Resource Attribute から測定値と設定値を取り除き、それらを既存の `growi.configs` info gauge のラベル、もしくは新規の system / process メトリクス群に再配置する。同時にコンテナ運用に耐える静的・動的メモリメトリクスを追加する。
-
-## Boundary Context
-
-- **In scope**:
-  - Resource Attribute から `os.totalmem` と `growi.attachment.type` を削除する
-  - 既存の `growi.configs` info gauge に `attachment_type` ラベルを追加する
-  - 新規メトリクス群(`system.memory.limit`, `system.host.memory.total`, `process.memory.usage`, `process.runtime.v8.heap.used` / `heap.total` / `heap.external`)を追加する
-  - 上記新規メトリクスを `setupCustomMetrics()` から起動する
-- **Out of scope**:
-  - 既存の `growi.*` メトリクス(`growi.pages.total`, `growi.users.total`, `growi.users.active`)の名称変更や再構成
-  - `growi.deployment.type` を OTel 標準 `deployment.environment.name` へ移行すること(identity として現状維持)
-  - CPU 系・ネットワーク系・GC・event loop lag などの追加メトリクス
-  - HTTP の anonymization layer(`http.target` 等の span attribute)の変更
-  - 外部パッケージ(OpenTelemetry コミュニティの汎用ホストメトリクス収集パッケージ等)の導入
-- **Adjacent expectations**:
-  - 上流: `growiInfoService.getGrowiInfo({ includeAttachmentInfo: true })` が `attachmentType` を供給し続けることを前提とする
-  - 下流: OpenTelemetry collector / Prometheus / Grafana 等の取り込み側ダッシュボードが、削除された Resource Attribute(`os.totalmem`, `growi.attachment.type`)から新しいメトリクス/ラベルへ参照を切り替えること。本仕様は切替対応に必要な変更内容の通知までを範囲とする
-
-## Requirements
-
-### Requirement 1: Resource Attribute の identity 専用化
-
-**Objective:** OpenTelemetry インフラ管理者として、GROWI が emit する Resource Attribute がホスト/サービスの identity 情報のみで構成されていることを保証したい。それにより Resource Attribute を本来のキー(テレメトリ発生元エンティティの一意識別)として運用できる。
-
-#### Acceptance Criteria
-
-1. When OpenTelemetry SDK が起動し Resource Attribute を構築する, the GROWI server shall `os.totalmem` を Resource Attribute に含めない.
-2. When OpenTelemetry SDK が起動し Resource Attribute を構築する, the GROWI server shall `growi.attachment.type` を Resource Attribute に含めない.
-3. The GROWI server shall 既存の identity 系 Resource Attribute(`os.type`, `os.platform`, `os.arch`, `growi.service.type`, `growi.deployment.type`, `service.name`, `service.version`, `service.instance.id`)の名称と値の意味を変更せずに emit する.
-
-### Requirement 2: Attachment storage backend を `growi.configs` のラベルとして公開
-
-**Objective:** 運用者として、GROWI が利用している attachment ストレージバックエンド(aws / gcs / gridfs / local / mongodb / azure 等)を従来の設定情報(`wiki_type`, `external_auth_types` 等)と同じ場所で参照したい。それによりインスタンス設定の確認を一つの info-gauge に集約できる。
-
-#### Acceptance Criteria
-
-1. When `growi.configs` observable gauge が観測される, the GROWI server shall 設定された attachment ストレージバックエンド種別を示す `attachment_type` ラベルを付与する.
-2. The GROWI server shall `growi.configs` gauge の既存ラベル(`site_url`, `site_url_hashed`, `wiki_type`, `external_auth_types`)の名称・値・付与条件を変更しない.
-3. If attachment ストレージバックエンドが特定できない場合, the GROWI server shall `attachment_type` ラベルの値を空文字とする(既存 `external_auth_types` の未取得時挙動と一致させる).
-
-### Requirement 3: コンテナ環境に対応したメモリ上限メトリクス
-
-**Objective:** コンテナ環境(Docker / Kubernetes)で GROWI を運用する管理者として、「コンテナに割り当てられたメモリ上限(cgroup limit)」と「ホストの物理メモリ総量」を別々のメトリクスとして参照したい。それにより、コンテナ単位のリソース逼迫とホスト単位の容量計画を区別して判断できる。
-
-#### Acceptance Criteria
-
-1. When メトリクス収集コールバックが発火し、実行プロセスに cgroup memory limit が設定されている, the GROWI server shall `system.memory.limit` メトリクスを cgroup memory limit のバイト数値で観測する(単位 `By`).
-2. If 実行プロセスに cgroup memory limit が設定されていない, the GROWI server shall 当該収集サイクルで `system.memory.limit` を観測しない.
-3. When メトリクス収集コールバックが発火する, the GROWI server shall `system.host.memory.total` メトリクスをホスト物理メモリの総バイト数で観測する(単位 `By`).
-
-### Requirement 4: プロセスおよび V8 ヒープのランタイムメトリクス
-
-**Objective:** 運用者として、GROWI プロセスのメモリ使用量と V8 ヒープの内訳を継続的に観測したい。それにより OOM やメモリリークの兆候を早期に検出できる。
-
-#### Acceptance Criteria
-
-1. When メトリクス収集コールバックが発火する, the GROWI server shall `process.memory.usage` メトリクスをプロセスの Resident Set Size のバイト数で観測する(単位 `By`).
-2. When メトリクス収集コールバックが発火する, the GROWI server shall `process.runtime.v8.heap.used` メトリクスを V8 ヒープの使用バイト数で観測する(単位 `By`).
-3. When メトリクス収集コールバックが発火する, the GROWI server shall `process.runtime.v8.heap.total` メトリクスを V8 ヒープの確保済み総バイト数で観測する(単位 `By`).
-4. When メトリクス収集コールバックが発火する, the GROWI server shall `process.runtime.v8.heap.external` メトリクスを V8 外部メモリ(external buffers 等)のバイト数で観測する(単位 `By`).
-
-### Requirement 5: 新規メトリクスモジュールのライフサイクル統合と耐障害性
-
-**Objective:** 運用者として、新規追加された system / process メトリクス群が既存カスタムメトリクスと同じタイミングで起動・登録され、かつ収集中の例外が他メトリクスの出力を巻き込まないことを保証したい。
-
-#### Acceptance Criteria
-
-1. When サーバー起動時に OpenTelemetry のカスタムメトリクスセットアップが実行される, the GROWI server shall 既存のカスタムメトリクス(application / user-counts / page-counts)と並んで新規 system / process メトリクスの登録を実行する.
-2. If 新規メトリクスの収集コールバック内で例外が発生する, the GROWI server shall 例外を吸収し、`diag` ロガーでエラーを記録した上で残りの収集サイクルを継続する.
-
-### Requirement 6: 後方互換性と運用コミュニケーション
-
-**Objective:** リリース管理者として、本リファクタリングが「Resource Attribute の削除と、メトリクス/ラベルの新規追加」のみで構成され、既存テレメトリ受信側にとって移行手順を伴うことを明示したい。
-
-#### Acceptance Criteria
-
-1. The GROWI server shall Requirement 1 で削除対象とされた 2 つの Resource Attribute 以外は、本仕様の前後で既存 Resource Attribute と既存メトリクス(`growi.configs`, `growi.users.total`, `growi.users.active`, `growi.pages.total`)の名称および値の意味を変更しない.
-2. The GROWI server shall 本仕様のリリースに際して、削除された Resource Attribute と新規メトリクス/ラベルの対応関係を運用者向けに明示する(リリースノートまたは PR 説明として、削除前後で参照先が分かる形式で記録する).

+ 0 - 127
.kiro/specs/otel-attributes-cleanup/research.md

@@ -1,127 +0,0 @@
-# Research & Design Decisions — otel-attributes-cleanup
-
-## Summary
-
-- **Feature**: `otel-attributes-cleanup`
-- **Discovery Scope**: Extension(既存 `features/opentelemetry/` モジュールの再編成 + 小規模追加)
-- **Key Findings**:
-  - Resource Attribute と Metric の責務分離が崩れており、`os.totalmem` と `growi.attachment.type` が Resource Attribute 側に紛れ込んでいる。
-  - GROWI のランタイム要件は Node.js `^24`(`apps/app/package.json` 経由)なので、`process.constrainedMemory()`(Node 20.12+)が無条件で利用可能。
-  - `@opentelemetry/host-metrics` パッケージは cgroup 非対応かつ `system.memory.limit` を emit しないため、要件を満たさず採用不可。
-
-## Research Log
-
-### 既存 ObservableGauge 実装パターン
-
-- **Context**: 新規 `system-metrics.ts` を既存パターンに整合させる必要がある。
-- **Sources Consulted**:
-  - `apps/app/src/features/opentelemetry/server/custom-metrics/application-metrics.ts`
-  - `apps/app/src/features/opentelemetry/server/custom-metrics/page-counts-metrics.ts`
-  - `apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.ts`
-- **Findings**:
-  - 各モジュールは `addXxxMetrics(): void` 関数を export する。
-  - `metrics.getMeter('growi-<scope>-metrics', '1.0.0')` で Meter を取得し、`createObservableGauge(name, { description, unit })` で gauge を作る。
-  - 観測は `meter.addBatchObservableCallback(async (result) => { ... }, [gauge, ...])` で登録し、try/catch でエラーを `diag.createComponentLogger(...)` 経由でログ出力する。
-  - 直接 `getMeter` の戻り値で `addBatchObservableCallback` を呼ぶ実装は無く、必ず `meter.` プレフィックスを介する。
-- **Implications**: 新規 `addSystemMetrics()` も同じシグネチャ・同じ Meter 命名・同じバッチコールバック構造で実装し、レビュー差分を最小化する。
-
-### 既存 spec.ts のモッキングパターン
-
-- **Context**: 新規モジュール用の spec.ts と既存 spec.ts 修正の作業負荷を見積もる必要がある。
-- **Sources Consulted**:
-  - `os-resource-attributes.spec.ts`(`vi.mock('node:os')` で stdlib をモック)
-  - `application-metrics.spec.ts`(`vi.mock('@opentelemetry/api')` で Meter / Gauge / diag をすべてモック)
-- **Findings**:
-  - `vi.mock('node:os')` で `totalmem` などの関数を `vi.fn()` 化する手法が確立済み。
-  - `vitest-mock-extended` の `mock<Meter>()` / `mock<ObservableGauge>()` で OpenTelemetry の型をモックし、`addBatchObservableCallback.mock.calls[0][0]` でコールバック関数を取り出して直接実行する。
-  - エラー時挙動は `mockGrowiInfoService.getGrowiInfo.mockRejectedValue(...)` → `expect(mockResult.observe).not.toHaveBeenCalled()` の形でテストされている。
-- **Implications**: 新規 `system-metrics.spec.ts` は `node:os` / `node:v8` / `node:process` を `vi.mock` し、`process.constrainedMemory` の 0/undefined 分岐を含めた網羅テストが可能。
-
-### `process.constrainedMemory()` の挙動
-
-- **Context**: cgroup limit が取れない環境での挙動を要件 3.2 に反映するため API 仕様を確定する。
-- **Sources Consulted**: Node.js v20.12 ドキュメント `process.constrainedMemory()`、Node.js v24 同 API ドキュメント。
-- **Findings**:
-  - 戻り値: `cgroup v1` / `cgroup v2` から取得した「プロセスに割り当てられたメモリ上限のバイト数」。
-  - cgroup が未設定 / detection 失敗時は `0` を返す。
-  - Node.js v19.6 で導入、v20.12 で stable、v24 でも継続。
-- **Implications**: 要件 3.1/3.2 の「設定されている / されていない」分岐は `value > 0` で判定する。`undefined` は実質的には返らないが、防御的に `value > 0` チェックでカバー可能。
-
-### Build vs Adopt: `@opentelemetry/host-metrics` 比較
-
-- **Context**: メトリクス追加の実装手段としてカスタム ObservableGauge を書くか、コミュニティパッケージを採用するか。
-- **Sources Consulted**: discovery 段階で別 subagent が実施した調査結果(最新 0.38.3、2026-02 リリース)。
-- **Findings**:
-  - cgroup 検出を行わず、`os.totalmem()`/`os.freemem()` 直読み。コンテナ環境での `system.memory.limit` を emit しない。
-  - V8 ヒープ統計を出力しない。
-  - 出力可否は `metricGroups` で粗いフィルタができるのみ。リネーム不可、属性追加不可。
-  - 追加 dep `systeminformation`、内包 semconv は 1.25.0(GROWI は 1.34.0)。
-- **Implications**: 採用しても要件 3.1(cgroup limit)と要件 4.2-4.4(V8 ヒープ)を別途自前で書く必要があり、結局二重実装になる。**カスタム ObservableGauge を選択**。
-
-## Architecture Pattern Evaluation
-
-| Option | Description | Strengths | Risks / Limitations | Notes |
-|--------|-------------|-----------|---------------------|-------|
-| Custom ObservableGauge (`system-metrics.ts`) | Node.js stdlib + 既存 `@opentelemetry/api` のみで自前実装 | 完全な制御、既存パターンと整合、追加 dep ゼロ | 約 50 行の実装と spec を書く必要 | **採用** |
-| `@opentelemetry/host-metrics` 採用 | コミュニティパッケージで `system.*`/`process.*` を自動 emit | 既製、network/CPU も無料で得られる | cgroup 未対応、V8 ヒープ非対応、semconv が古い、不要メトリクス強制 emit | 不採用(要件未充足) |
-| `os.totalmem` を削除のみで終了 | 新規メトリクスを追加せず Resource Attribute 削除だけ | 最小工数 | 要件 3/4(コンテナ運用メトリクス)が満たされない | 不採用 |
-
-## Design Decisions
-
-### Decision: `system.memory.limit` と `system.host.memory.total` を別メトリクスに分離
-
-- **Context**: コンテナ環境で「コンテナの上限」と「ホストの物理メモリ」のどちらを参照したいかは運用観点が異なる。単一メトリクスでは判別が困難。
-- **Alternatives Considered**:
-  1. 単一メトリクス `system.memory.limit` を cgroup → fallback で `os.totalmem` にする。
-  2. `system.memory.limit` と `system.host.memory.total` を別メトリクスにする。
-- **Selected Approach**: 2。`system.memory.limit` は cgroup limit が取れたときのみ観測、`system.host.memory.total` は常に観測。
-- **Rationale**: 「コンテナ上限の有無」自体が運用上の情報となる。fallback されると bare-metal でも cgroup でも同じシリーズに混在し、ダッシュボード側で見分けが付かない。
-- **Trade-offs**: 出力メトリクス数が 1 つ増えるが、運用観点での明瞭さが勝る。
-- **Follow-up**: ダッシュボード移行時の運用者向け説明(リリースノート相当)に「cgroup limit 未設定では `system.memory.limit` が emit されない」を明記する。
-
-### Decision: `growi.attachment.type` を `growi.configs` のラベルへ統合
-
-- **Context**: `growi.attachment.type` は「サブシステム設定値」であり identity ではない。同等の設定情報(`wiki_type`, `external_auth_types`)は既に `growi.configs` info gauge のラベルに集約されている。
-- **Alternatives Considered**:
-  1. Resource Attribute に残す。
-  2. `growi.configs` ラベルへ統合(Prometheus info パターン)。
-  3. 独立した info gauge を新設。
-- **Selected Approach**: 2。既存ラベル群(snake_case)に揃え `attachment_type` として追加。
-- **Rationale**: 「インスタンス設定」を一箇所で見られるという既存設計意図に沿う。Resource Attribute は identity 専用に整理できる。
-- **Trade-offs**: `growi.configs` のラベル数が増える(4 → 5)。カーディナリティ影響は限定的(attachment.type は固定 enum)。
-- **Follow-up**: ラベル命名は `attachment_type`(snake_case)に統一。値の取得不能時は空文字 `''` フォールバック(既存 `external_auth_types` と整合)。
-
-### Decision: `growi.deployment.type` は現状維持
-
-- **Context**: OTel 標準には `deployment.environment.name`("production"/"staging" 等)があるが、GROWI の `growi.deployment.type`("docker"/"k8s"/"growi-docker-compose" 等)はランタイム形態を表し、環境分類とは別概念。
-- **Alternatives Considered**:
-  1. `deployment.environment.name` に寄せる。
-  2. `growi.deployment.type` のまま据え置く。
-- **Selected Approach**: 2。Resource Attribute として現状維持。
-- **Rationale**: 値の意味が semconv 標準と乖離するため、無理に標準名を当てると誤解を招く。
-- **Follow-up**: 将来的に「環境(prod/stg)」の表現が必要になった時点で別属性として導入する。
-
-### Decision: 単一 Meter `growi-system-metrics` で system / process / V8 を束ねる
-
-- **Context**: 既存パターンでは目的別に Meter を分けている(`growi-application-metrics`, `growi-user-counts-metrics`, `growi-page-counts-metrics`)。
-- **Alternatives Considered**:
-  1. `growi-system-metrics` 単一 Meter で 6 メトリクスをまとめる。
-  2. `growi-system-metrics` と `growi-process-metrics` に Meter を分ける。
-- **Selected Approach**: 1。
-- **Rationale**: いずれも「ランタイム / ホストのリソース観測」という単一目的であり、`system.*`/`process.*` の prefix で十分名前空間が分離できる。Meter を分けると `addBatchObservableCallback` の呼び出しと spec も二重になり、管理コスト増。
-- **Trade-offs**: 将来「process 系のみオフにする」のような細かい制御が難しくなるが、現時点で必要性なし。
-
-## Risks & Mitigations
-
-- **下流ダッシュボードの参照切れ** — 削除される Resource Attribute(`os.totalmem`, `growi.attachment.type`)を引いていたクエリは値を返さなくなる。**Mitigation**: PR 説明とリリースノートに「Removed → Replaced by」の対応表を記載(要件 6.2)。
-- **`process.constrainedMemory()` のプラットフォーム依存** — Linux cgroup v1/v2 のみサポートで、macOS/Windows では常に 0 を返す。**Mitigation**: 0 のときは `system.memory.limit` を観測しないという挙動(要件 3.2)が、そのまま非対応プラットフォームの振る舞いと一致するため追加対策不要。`os.totalmem()` 経由の `system.host.memory.total` は全プラットフォーム共通で動作する。
-- **新規メトリクスのカーディナリティ** — 新しい 6 メトリクスは label を持たない gauge であり、追加カーディナリティ寄与はインスタンス分のみ。**Mitigation**: 設計上追加の attribute を付与しないことを徹底(要件 4 系の AC が暗黙に保証)。
-- **テストの cgroup mock 容易性** — `process.constrainedMemory` は `vi.mock` でモックすることになるが、Node.js グローバル `process` をモックするのは記法に注意が必要。**Mitigation**: `vi.spyOn(process, 'constrainedMemory').mockReturnValue(...)` を使う方針を spec パターンとして確立する。
-
-## References
-
-- [Node.js process.constrainedMemory()](https://nodejs.org/api/process.html#processconstrainedmemory) — cgroup ベースのメモリ上限取得 API。
-- [Node.js v8.getHeapStatistics()](https://nodejs.org/api/v8.html#v8getheapstatistics) — V8 ヒープ統計取得 API。
-- [OpenTelemetry semantic conventions — system memory](https://opentelemetry.io/docs/specs/semconv/system/system-metrics/#metric-systemmemoryusage) — `system.memory.*` の semconv。
-- [OpenTelemetry semantic conventions — process](https://opentelemetry.io/docs/specs/semconv/runtime-environment/process/) — `process.memory.usage` の semconv。
-- 既存実装: `apps/app/src/features/opentelemetry/server/custom-metrics/application-metrics.ts` — ObservableGauge + addBatchObservableCallback のリファレンス実装。

+ 0 - 57
.kiro/specs/otel-attributes-cleanup/tasks.md

@@ -1,57 +0,0 @@
-# Implementation Plan
-
-- [ ] 1. Resource Attribute cleanup
-- [x] 1.1 (P) Remove os.totalmem from OS resource attributes
-  - `os-resource-attributes.ts` の `osInfo` オブジェクトおよび返り値 attributes から `totalmem` 関連の行を削除する。`os.type` / `os.platform` / `os.arch` は維持する。
-  - `os-resource-attributes.spec.ts` の `vi.mock('node:os')` のスタブから `totalmem: vi.fn()` を除去し、3 つの既存テストケースから `os.totalmem` 関連の期待値および `mockOs.totalmem` 呼び出し検証を削除する。
-  - 完了状態: `pnpm vitest run os-resource-attributes.spec` がパスし、`getOsResourceAttributes()` の戻り値が `os.type` / `os.platform` / `os.arch` の 3 キーのみとなる。
-  - _Requirements: 1.1, 1.3_
-  - _Boundary: OsResourceAttributes_
-
-- [x] 1.2 (P) Remove growi.attachment.type from application resource attributes
-  - `application-resource-attributes.ts` の返り値 attributes から `'growi.attachment.type'` 行を削除する。
-  - 同ファイル内の `growiInfoService.getGrowiInfo({ includeAttachmentInfo: true })` 呼び出しから `includeAttachmentInfo: true` を除去する(このモジュールからは `attachmentType` を参照しなくなるため)。
-  - `application-resource-attributes.spec.ts` から `growi.attachment.type` 関連の期待値・スタブを削除する。
-  - 完了状態: `pnpm vitest run application-resource-attributes.spec` がパスし、`getApplicationResourceAttributes()` の戻り値が `growi.service.type` / `growi.deployment.type` のみとなる。
-  - _Requirements: 1.2, 1.3_
-  - _Boundary: ApplicationResourceAttributes_
-
-- [x] 2. (P) Add attachment_type label to growi.configs info gauge
-  - `application-metrics.ts` の `result.observe(growiInfoGauge, 1, { ... })` のラベルオブジェクトに `attachment_type: growiInfo.additionalInfo?.attachmentType ?? ''` を追加する。既存の `getGrowiInfo({ includeAttachmentInfo: true })` 呼び出しはそのまま維持する。
-  - `application-metrics.spec.ts` に attachment_type ラベル付与の検証を追加する: 通常ケース(例: `attachmentType: 'aws'`)と未取得フォールバックケース(`additionalInfo: undefined` で `attachment_type: ''`)の 2 系統。
-  - 既存テストケース(site_url / site_url_hashed / wiki_type / external_auth_types)の期待ラベルオブジェクトに `attachment_type` を追加し、5 ラベル並存を明示的に検証する。
-  - 完了状態: `pnpm vitest run application-metrics.spec` がパスし、`growi.configs` gauge の observe 第 3 引数が 5 キー(既存 4 + `attachment_type`)を持つことが確認される。
-  - _Requirements: 2.1, 2.2, 2.3_
-  - _Boundary: ApplicationMetrics_
-
-- [x] 3. (P) Implement and test SystemMetrics module
-  - `apps/app/src/features/opentelemetry/server/custom-metrics/system-metrics.ts` を新規作成し、`addSystemMetrics(): void` を export する。`loggerFactory('growi:opentelemetry:custom-metrics:system')` と `diag.createComponentLogger({ namespace: 'growi:custom-metrics:system' })` を既存 `application-metrics.ts` と同様のパターンで初期化する。
-  - 単一 Meter `growi-system-metrics`(version `'1.0.0'`)を `metrics.getMeter` で取得し、`createObservableGauge` で 6 つの gauge を作成する: `system.memory.limit`, `system.host.memory.total`, `process.memory.usage`, `process.runtime.v8.heap.used`, `process.runtime.v8.heap.total`, `process.runtime.v8.heap.external`。すべて unit は `By`。
-  - 1 つの `addBatchObservableCallback` 内で `process.constrainedMemory()` / `os.totalmem()` / `process.memoryUsage()` / `v8.getHeapStatistics()` を 1 回ずつ呼び、戻り値をローカル変数に保持してから各 gauge を `result.observe(...)` で観測する。`process.constrainedMemory()` の戻り値が 0 もしくは falsy のときは `system.memory.limit` のみスキップし、他 5 メトリクスは常に観測する。
-  - コールバック全体を try/catch で囲む。例外時は `loggerDiag.error('Failed to collect system metrics', { error })` を呼び、`result.observe` を一切呼ばずに return する。
-  - `system-metrics.spec.ts` を新規作成し、以下を網羅する: (a) Meter 名 `growi-system-metrics` と version `'1.0.0'` での `metrics.getMeter` 呼び出し検証、(b) 6 つの `createObservableGauge` の name + unit `By` の検証、(c) `process.constrainedMemory()` が正値時に `system.memory.limit` を当該値で観測、(d) 戻り値 `0` 時に `system.memory.limit` のみスキップしつつ他 5 メトリクスは観測、(e) `process.memoryUsage().rss` / `.external` および `v8.getHeapStatistics().used_heap_size` / `.total_heap_size` が対応 gauge へ正しくマップされる、(f) コールバック内例外時に `loggerDiag.error` が呼ばれ `observe` が 1 度も呼ばれない。`vi.mock('node:os')` / `vi.mock('node:v8')` および `vi.spyOn(process, 'constrainedMemory')` / `vi.spyOn(process, 'memoryUsage')` を使用する。
-  - 完了状態: `pnpm vitest run system-metrics.spec` がパスし、新規モジュール `system-metrics.ts` が上記 6 メトリクスと cgroup 分岐・エラーハンドリングを正しく実装している。
-  - _Requirements: 3.1, 3.2, 3.3, 4.1, 4.2, 4.3, 4.4, 5.2_
-  - _Boundary: SystemMetrics_
-
-- [x] 4. Wire addSystemMetrics into setupCustomMetrics
-  - `custom-metrics/index.ts` のトップに `export { addSystemMetrics } from './system-metrics';` を追加する。
-  - `setupCustomMetrics()` 内で既存 3 関数(`addApplicationMetrics`, `addUserCountsMetrics`, `addPageCountsMetrics`)と同じ dynamic import パターンで `system-metrics` をロードし、`addSystemMetrics()` を呼ぶ。
-  - 完了状態: サーバー起動時のログに `growi:opentelemetry:custom-metrics:system` namespace の "Starting system metrics collection" / "...started successfully" が出力される。`pnpm vitest run` で既存の opentelemetry 関連テスト(特に node-sdk.spec)がパスする。
-  - _Depends: 3_
-  - _Requirements: 5.1_
-  - _Boundary: CustomMetricsIndex_
-
-- [ ] 5. Project-wide verification and operator handoff
-- [x] 5.1 Verify lint, typecheck, tests, and build pass
-  - `turbo run lint --filter @growi/app` を実行し、Biome / TypeScript エラーがないことを確認する。
-  - `turbo run test --filter @growi/app` を実行し、変更対象の 4 spec(os-resource-attributes / application-resource-attributes / application-metrics / system-metrics)および既存全テストがパスすることを確認する。
-  - `turbo run build --filter @growi/app` を実行し、Turbopack による本番ビルドがエラーなく完了することを確認する。
-  - 完了状態: 上記 3 コマンドすべてが exit code 0 で終了する。
-  - _Requirements: 6.1_
-
-- [x] 5.2 Author operator migration mapping in PR description
-  - PR 本文に「削除 Resource Attribute → 代替メトリクス/ラベル」の対応表を記載する。具体的には `os.totalmem` → `system.host.memory.total` および `system.memory.limit`(cgroup 設定時)の 2 メトリクス、`growi.attachment.type` → `growi.configs` の `attachment_type` ラベル、の 2 行。
-  - 新規追加された 6 メトリクスの一覧(名前と単位 `By`)を併記する。
-  - 完了状態: otel-infra 管理者が PR 本文 1 ページ内で「何が消え、どこに移ったか」「新たに何が出るようになったか」をワンビューで把握できる状態となる。
-  - _Requirements: 6.2_

+ 0 - 123
apps/app/src/features/opentelemetry/docs/custom-metrics/architecture.md

@@ -1,123 +0,0 @@
-# OpenTelemetry Custom Metrics Architecture
-
-## 概要
-
-GROWIのOpenTelemetryカスタムメトリクスは、以下の3つのカテゴリに分類して実装されています:
-
-1. **Resource Attributes** - システム起動時に設定される静的情報
-2. **Config Metrics** - 設定変更により動的に変わる可能性があるメタデータ
-3. **Custom Metrics** - 時間と共に変化する業務メトリクス
-
-## アーキテクチャ
-
-### Resource Attributes
-
-静的なシステム情報をOpenTelemetryのResource Attributesとして設定します。Resource Attributesは2段階で設定されます:
-
-1. **起動時設定**: OS情報など、データベースアクセスが不要な静的情報
-2. **データベース初期化後設定**: アプリケーション情報など、データベースアクセスが必要な情報
-
-#### 実装場所
-```
-src/features/opentelemetry/server/custom-resource-attributes/
-├── os-resource-attributes.ts        # OS情報 (起動時設定)
-└── application-resource-attributes.ts  # アプリケーション固定情報 (DB初期化後設定)
-```
-
-#### OS情報 (`os-resource-attributes.ts`) - 起動時設定
-- `os.type` - OS種別 (Linux, Windows等)
-- `os.platform` - プラットフォーム (linux, darwin等)
-- `os.arch` - アーキテクチャ (x64, arm64等)
-- `os.totalmem` - 総メモリ量
-
-#### アプリケーション固定情報 (`application-resource-attributes.ts`) - DB初期化後設定
-- `growi.service.type` - サービスタイプ
-- `growi.deployment.type` - デプロイメントタイプ
-- `growi.attachment.type` - ファイルアップロードタイプ
-- `growi.installedAt` - インストール日時
-- `growi.installedAt.by_oldest_user` - 最古ユーザー作成日時
-
-### Config Metrics
-
-設定変更により動的に変わる可能性があるメタデータ実装します。値は常に1で、情報はラベルに格納されます。
-
-#### 実装場所
-```
-src/features/opentelemetry/server/custom-metrics/application-metrics.ts
-```
-
-#### 収集される情報
-- `service_instance_id` - サービスインスタンス識別子
-- `site_url` - サイトURL
-- `wiki_type` - Wiki種別 (open/closed)
-- `external_auth_types` - 有効な外部認証プロバイダー
-
-#### メトリクス例
-```
-growi_info{service_instance_id="abc123",site_url="https://wiki.example.com",wiki_type="open",external_auth_types="github,google"} 1
-```
-
-### Custom Metrics
-
-時間と共に変化する業務メトリクスを実装します。数値として監視・アラートの対象となるメトリクスです。
-
-#### 実装場所
-```
-src/features/opentelemetry/server/custom-metrics/
-├── application-metrics.ts  # Config Metrics (既存)
-└── user-counts-metrics.ts  # ユーザー数メトリクス (新規作成)
-```
-
-#### ユーザー数メトリクス (`user-counts-metrics.ts`)
-- `growi.users.total` - 総ユーザー数
-- `growi.users.active` - アクティブユーザー数
-
-## 収集間隔・設定タイミング
-
-### Resource Attributes
-- **OS情報**: アプリケーション起動時に1回のみ設定
-- **アプリケーション情報**: データベース初期化後に1回のみ設定
-
-### Metrics
-- **Config Metrics**: 60秒間隔で収集 (デフォルト)
-- **Custom Metrics**: 60秒間隔で収集 (デフォルト)
-
-### 2段階設定の理由
-
-Resource Attributesが2段階で設定される理由:
-
-1. **循環依存の回避**: アプリケーション情報の取得にはgrowiInfoServiceが必要だが、OpenTelemetry初期化時点では利用できない
-2. **データベース依存**: インストール日時やサービス設定などはデータベースから取得する必要がある
-3. **起動時間の最適化**: データベース接続を待たずにOpenTelemetryの基本機能を開始できる
-
-## 設定の変更
-
-メトリクス収集間隔は `PeriodicExportingMetricReader` の `exportIntervalMillis` で変更可能です:
-
-```typescript
-metricReader: new PeriodicExportingMetricReader({
-  exporter: new OTLPMetricExporter(),
-  exportIntervalMillis: 30000, // 30秒間隔
-}),
-```
-
-## 使用例
-
-### Prometheusでのクエリ例
-
-```promql
-# 総ユーザー数の推移
-growi_users_total
-
-# Wiki種別でグループ化した情報
-growi_info{wiki_type="open"}
-
-# 外部認証を使用しているインスタンス
-growi_info{external_auth_types!=""}
-```
-
-### Grafanaでの可視化例
-
-- ユーザー数の時系列グラフ
-- Wiki種別の分布円グラフ
-- 外部認証プロバイダーの利用状況

+ 0 - 87
apps/app/src/features/opentelemetry/docs/custom-metrics/implementation-guide.md

@@ -1,87 +0,0 @@
-# OpenTelemetry Custom Metrics Implementation Guide
-
-## 改修実装状況
-
-### ✅ 完了した実装
-
-#### 1. Resource Attributes
-- **OS情報**: `src/features/opentelemetry/server/custom-resource-attributes/os-resource-attributes.ts`
-  - OS種別、プラットフォーム、アーキテクチャ、総メモリ量
-  - 起動時に設定
-- **アプリケーション固定情報**: `src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.ts`
-  - サービス・デプロイメントタイプ、添付ファイルタイプ、インストール情報
-  - データベース初期化後に設定
-
-#### 2. Config Metrics
-- **実装場所**: `src/features/opentelemetry/server/custom-metrics/application-metrics.ts`
-- **メトリクス**: `growi.configs` (値は常に1、情報はラベルに格納)
-- **収集情報**: サービスインスタンスID、サイトURL、Wiki種別、外部認証タイプ
-
-#### 3. Custom Metrics
-- **実装場所**: `src/features/opentelemetry/server/custom-metrics/user-counts-metrics.ts`
-- **メトリクス**: 
-  - `growi.users.total` - 総ユーザー数
-  - `growi.users.active` - アクティブユーザー数
-
-#### 4. 統合作業
-- **node-sdk-configuration.ts**: OS情報のResource Attributes統合済み
-- **node-sdk.ts**: データベース初期化後のアプリケーション情報設定統合済み
-- **メトリクス初期化**: Config MetricsとCustom Metricsの初期化統合済み
-
-### 📋 実装済みの統合
-
-#### Resource Attributesの2段階設定
-
-**1段階目 (起動時)**: `generateNodeSDKConfiguration`
-```typescript
-// OS情報のみでResourceを作成
-const osAttributes = getOsResourceAttributes();
-resource = resourceFromAttributes({
-  [ATTR_SERVICE_NAME]: 'growi',
-  [ATTR_SERVICE_VERSION]: version,
-  ...osAttributes,
-});
-```
-
-**2段階目 (DB初期化後)**: `setupAdditionalResourceAttributes`
-```typescript
-// アプリケーション情報とサービスインスタンスIDを追加
-const appAttributes = await getApplicationResourceAttributes();
-if (serviceInstanceId != null) {
-  appAttributes[ATTR_SERVICE_INSTANCE_ID] = serviceInstanceId;
-}
-const updatedResource = await generateAdditionalResourceAttributes(appAttributes);
-setResource(sdkInstance, updatedResource);
-```
-
-#### メトリクス収集の統合
-```typescript
-// generateNodeSDKConfiguration内で初期化
-addApplicationMetrics();
-addUserCountsMetrics();
-```
-
-## ファイル構成
-
-```
-src/features/opentelemetry/server/
-├── custom-resource-attributes/
-│   ├── index.ts                           # エクスポート用インデックス
-│   ├── os-resource-attributes.ts          # OS情報
-│   └── application-resource-attributes.ts # アプリケーション情報
-├── custom-metrics/
-│   ├── application-metrics.ts             # Config Metrics (更新済み)
-│   └── user-counts-metrics.ts             # ユーザー数メトリクス (新規)
-└── docs/
-    ├── custom-metrics-architecture.md     # アーキテクチャ文書
-    └── implementation-guide.md            # このファイル
-```
-
-## 設計のポイント
-
-1. **2段階Resource設定**: データベース依存の情報は初期化後に設定して循環依存を回避
-2. **循環依存の回避**: 動的importを使用してgrowiInfoServiceを読み込み
-3. **エラーハンドリング**: 各メトリクス収集でtry-catchを実装
-4. **型安全性**: Optional chainingを使用してundefinedを適切に処理
-5. **ログ出力**: デバッグ用のログを各段階で出力
-6. **起動時間の最適化**: データベース接続を待たずにOpenTelemetryの基本機能を開始

+ 0 - 49
apps/app/src/features/opentelemetry/docs/overview.md

@@ -1,49 +0,0 @@
-# OpenTelemetry Overview
-
-## 現在の実装状況
-
-### 基本機能
-- ✅ **Trace収集**: HTTP、Database等の自動インストルメンテーション
-- ✅ **Metrics収集**: 基本的なアプリケーションメトリクス
-- ✅ **OTLP Export**: gRPCでのデータ送信
-- ✅ **設定管理**: 環境変数による有効/無効制御
-
-### アーキテクチャ
-```
-[GROWI App] → [NodeSDK] → [Auto Instrumentations] → [OTLP Exporter] → [Collector]
-```
-
-### 実装ファイル
-| ファイル | 責務 |
-|---------|------|
-| `node-sdk.ts` | SDK初期化・管理 |
-| `node-sdk-configuration.ts` | 設定生成 |
-| `node-sdk-resource.ts` | リソース属性管理 |
-| `logger.ts` | 診断ログ |
-
-### 設定項目
-| 環境変数 | デフォルト | 説明 |
-|---------|-----------|------|
-| `OTEL_ENABLED` | `false` | 有効/無効 |
-| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | エクスポート先 |
-| `OTEL_SERVICE_NAME` | `growi` | サービス名 |
-| `OTEL_SERVICE_VERSION` | 自動 | バージョン |
-
-### データフロー
-1. **Auto Instrumentation** でHTTP/DB操作を自動計測
-2. **NodeSDK** がスパン・メトリクスを収集
-3. **OTLP Exporter** が外部Collectorに送信
-
-## 制限事項
-- 機密データの匿名化未実装
-- GROWIアプリ固有の情報未送信
-
-## 参考情報
-- [OpenTelemetry Node.js SDK](https://open-telemetry.github.io/opentelemetry-js/)
-- [Custom Metrics Documentation](https://opentelemetry.io/docs/instrumentation/js/manual/#creating-metrics)
-- [HTTP Instrumentation Configuration](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http#configuration)
-- [Semantic Conventions for System Metrics](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md)
-- [Resource Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md)
-
----
-*更新日: 2025-06-19*