Browse Source

docs(spec): add otel-attributes-cleanup spec

Resource Attribute と Custom Metric の責務分離を整理し、コンテナ運用向けのメモリ系メトリクスを追加するための spec。Resource Attribute から `os.totalmem` と `growi.attachment.type` を削除し、それらを `growi.configs` info gauge のラベル / 新規 `system-metrics.ts` モジュールへ再配置する方針を記述。

- brief.md: 棚卸し結果と判断
- requirements.md: EARS 形式の 6 要件
- design.md: コンポーネント設計と File Structure Plan
- research.md: build-vs-adopt 比較と設計判断ログ

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yuki Takei 1 day ago
parent
commit
7d7cf9d464

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

@@ -0,0 +1,91 @@
+# 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.

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

@@ -0,0 +1,411 @@
+# 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 の挙動確認)。

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

@@ -0,0 +1,87 @@
+# 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 説明として、削除前後で参照先が分かる形式で記録する).

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

@@ -0,0 +1,127 @@
+# 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 のリファレンス実装。

+ 22 - 0
.kiro/specs/otel-attributes-cleanup/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "otel-attributes-cleanup",
+  "created_at": "2026-05-21T00:00:00.000Z",
+  "updated_at": "2026-05-21T02:00:00.000Z",
+  "language": "ja",
+  "phase": "design-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": false
+    },
+    "tasks": {
+      "generated": false,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}