Purpose: GROWI の OpenTelemetry 統合 (apps/app/src/features/opentelemetry/) を、SDK ライフサイクル / Resource Attribute / Custom Metric / HTTP Anonymization の 4 レイヤに分けて責務境界を明文化する大局的なメンテナンス spec。
Users:
Impact: 既に稼働している実装の現状をスナップショットとして固定化する。新規実装はゼロで、コード変更は伴わない。将来の機能追加・変更は本 spec の Boundary Commitments に従って境界の中で行われる。
growi.configs ラベル、観測値は growi.* / system.* / process.* メトリクスへ、というレイヤ分離を維持する。@opentelemetry/host-metrics 等への置き換え。service.instance.id の自動生成(現状は otel:serviceInstanceId か app:serviceInstanceId の config 値を passthrough する)。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()。sdkInstance モジュール変数)。custom-resource-attributes/os-resource-attributes.ts の getOsResourceAttributes(): Attributes。custom-resource-attributes/application-resource-attributes.ts の getApplicationResourceAttributes(): Promise<Attributes>。custom-resource-attributes/index.ts のバレル。os.type, os.platform, os.arch, growi.service.type, growi.deployment.type。service.instance.id および application info)の責務。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})。growi-<scope>-metrics, version '1.0.0'。ObservableGauge + addBatchObservableCallback + try/catch + diag.createComponentLogger で例外吸収。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 の登録順)。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 出力。semconv.ts の ATTR_SERVICE_INSTANCE_ID / ATTR_HTTP_TARGET 定義。~/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 の責務)。http.target 以外)。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 しない。apps/app/.next/node_modules/ 残留有無の確認と dependencies 分類が必要(.claude/rules/package-dependencies.md 参照)。@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 に切り替える。engines.node が ^24 未満)→ process.constrainedMemory() / v8.getHeapStatistics() の互換性再確認。features/opentelemetry/server/ は以下の 5 レイヤを下流方向の単方向依存で構成する:
node-sdk.ts が node-sdk-configuration.ts / node-sdk-resource.ts / logger.ts および custom-metrics/index.ts を統括する。custom-resource-attributes/ を node-sdk-configuration.ts の 2 段階目(generateAdditionalResourceAttributes)が consumer として呼ぶ。custom-metrics/index.ts の setupCustomMetrics() が起動時に 4 モジュールを順次登録。各モジュールは growiInfoService または Node.js stdlib を参照。anonymization/index.ts から export される httpInstrumentationConfig が node-sdk-configuration.ts の auto-instrumentation 構築時に注入される。semconv.ts は Layer 1, 2, 4 から参照される葉ノード。各レイヤは独立に拡張可能で、横断的な相互依存(Custom Metric が Anonymization に依存する等)は存在しない。
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
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()
| 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>() パターン |
既存テスト基盤 |
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
custom-metrics/<scope>-metrics.ts を新規作成し、addXxxMetrics(): void を export する。loggerFactory('growi:opentelemetry:custom-metrics:<scope>') と diag.createComponentLogger({ namespace: 'growi:custom-metrics:<scope>' }) を初期化する。metrics.getMeter('growi-<scope>-metrics', '1.0.0') で Meter 取得。meter.createObservableGauge(name, { description, unit }) で gauge 群を作成。meter.addBatchObservableCallback(async (result) => { try { ... } catch (e) { loggerDiag.error(...) } }, [...gauges]) を 1 つ登録。custom-metrics/index.ts の barrel に export { addXxxMetrics } from './<scope>-metrics'; を追加し、setupCustomMetrics() 内で dynamic import + 呼び出し。*.spec.ts を co-locate し、vi.mock('@opentelemetry/api') + mock<Meter>() パターンで unit test を書く。anonymization/handlers/<scope>-handler.ts を新規作成し、AnonymizationModule 型の object を export する。canHandle(url): boolean で対象 URL を判別する。先頭一致 / URL parser / 正規表現を適宜使用。handle(request, url): Record<string, string> | null で anonymizeQueryParams() または独自ロジックを適用し、{ [ATTR_HTTP_TARGET]: anonymizedUrl } を返す。何も匿名化しない場合は null。handlers/index.ts の anonymizationModules 配列に追加。順序が重要: より具体的なパスを先に置く(API > 静的 page access)。*.spec.ts を co-locate し、canHandle の境界条件と handle の URL 変換を網羅する。| 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 参照) |
| Field | Detail |
|---|---|
| Intent | OpenTelemetry SDK のプロセス内ライフサイクル管理 |
| Requirements | 1.1, 1.2, 1.3, 1.4, 8.1, 8.2, 8.3 |
Responsibilities & Constraints
NodeSDK インスタンスのみを保持する。otel:enabled が false のときは SDK を構築しない。OTEL_SDK_DISABLED env var と otel:enabled の食い違いを warn で報告し上書きする。setResource() の private API 経由で 2 段階目を反映する。start() 直後に setupCustomMetrics() を呼び出して Custom Metric の登録を行う。Dependencies
apps/app/src/server/app.ts 系の起動シーケンス(実体は本 spec の外)。@opentelemetry/sdk-node (NodeSDK), configManager, ./node-sdk-configuration, ./node-sdk-resource, ./logger, ./custom-metrics.export const initInstrumentation: () => Promise<void>;
export const setupAdditionalResourceAttributes: () => Promise<void>;
export const startOpenTelemetry: () => void;
// テスト専用
export const __testing__: { getSdkInstance, reset };
Implementation Notes
let sdkInstance: NodeSDK | undefined; を見て、設定済みなら warn のみで return。start() 前後で instrumentationEnabled を再確認する(dev 時に env を切り替える運用への配慮)。setResource() は NodeSDK._resource を直接書き換える。OpenTelemetry SDK が public な resource 上書き API を提供したら撤去する候補(Revalidation Trigger)。| Field | Detail |
|---|---|
| Intent | NodeSDKConfiguration オブジェクトと Resource の構築 |
| Requirements | 2.1, 2.2, 2.3, 6.1, 6.5, 8.1, 8.2, 8.3 |
Responsibilities & Constraints
{ service.name: 'growi', service.version: <growi-version> }。{ service.instance.id?, ...osAttrs, ...appAttrs } を merge。OTLPTraceExporter()(引数なし)。PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter(), exportIntervalMillis: 300000 })。getNodeAutoInstrumentations({ pino: disabled, fs: disabled, http: { enabled, ...httpInstrumentationConfig } })。enableAnonymization が true のときのみ httpInstrumentationConfig を注入。Dependencies
@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.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。| 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)。| 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 レベルにマップ。| 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
node:os (type(), platform(), arch()).export function getOsResourceAttributes(): Attributes;
// 戻り値: { 'os.type': string, 'os.platform': string, 'os.arch': string }
| 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 ラベル側の責務)。growiInfoService の失敗を吸収し、空 attributes を返す。Dependencies
~/server/service/growi-info の growiInfoService.getGrowiInfo({})(dynamic import で循環依存を回避)。export async function getApplicationResourceAttributes(): Promise<Attributes>;
// 戻り値: { 'growi.service.type': string, 'growi.deployment.type': string }
| 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
growiInfoService.getGrowiInfo({ includeAttachmentInfo: true }), configManager.getConfig('otel:isAppSiteUrlHashed').export function addApplicationMetrics(): void;
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 |
| 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。export function addUserCountsMetrics(): void;
| 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。export function addPageCountsMetrics(): void;
| Field | Detail |
|---|---|
| Intent | コンテナ / ホスト / プロセス / V8 ヒープのメモリ系統計を ObservableGauge で出力 |
| Requirements | 5.1, 5.2, 5.3, 5.4, 5.5 |
Responsibilities & Constraints
growi-system-metrics(version '1.0.0')で 6 つの ObservableGauge を作成。すべて単位 'By'。addBatchObservableCallback で process.constrainedMemory() / os.totalmem() / process.memoryUsage() / v8.getHeapStatistics() を 1 回ずつ呼び、ローカル変数経由で 6 個の gauge に観測値を割り振る。process.constrainedMemory() が > 0 のときのみ system.memory.limit を観測、0 または falsy のときは当該 gauge のみスキップし他 5 個は観測する。loggerDiag.error('Failed to collect system metrics', { error }) を呼んで result.observe を一切呼ばずに return。| 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 |
— |
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 でアクセスする。| Field | Detail |
|---|---|
| Intent | Custom Metric モジュール群の起動合成点 |
| Requirements | 4.1–4.4, 10.1 |
Responsibilities & Constraints
addApplicationMetrics() / addUserCountsMetrics() / addPageCountsMetrics() / addSystemMetrics() を順次呼ぶ。export const setupCustomMetrics: () => Promise<void>;
export { addApplicationMetrics, addPageCountsMetrics, addSystemMetrics, addUserCountsMetrics };
| 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 のときのみ行う。export const httpInstrumentationConfig: InstrumentationConfigMap['@opentelemetry/instrumentation-http'];
| Field | Detail |
|---|---|
| Intent | 個別 anonymization handler の共通契約 |
| Requirements | 6.1, 6.2, 6.3, 6.4, 10.2 |
export interface AnonymizationModule {
canHandle(url: string): boolean;
handle(request: IncomingMessage, url: string): Record<string, string> | null;
}
| Field | Detail |
|---|---|
| Intent | 登録済み handler のコレクション。配列順 = 評価順で、より具体的なパスから書く。 |
| Requirements | 6.2, 6.3, 6.4, 10.2 |
Registration Order
searchApiModule — 検索 APIpageListingApiModule — page-listing APIpageApiModule — pages/list 系 APIpageAccessModule — 非 API ページアクセス(最も汎用的なため最後)| Field | Detail |
|---|---|
| Intent | /_api/search, /_search の q クエリパラメータを匿名化 |
| Requirements | 6.2 |
canHandle: /\/_api\/search(\?|$)/ または /\/_search(\?|$)/ または '/_api/search/' / '/_search/' を含む。
handle: q パラメータが含まれていれば anonymizeQueryParams(url, ['q'])。
| Field | Detail |
|---|---|
| Intent | /_api/v3/page-listing/{ancestors-children,children,item} の path パラメータを匿名化 |
| Requirements | 6.3 |
| 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 |
| Field | Detail |
|---|---|
| Intent | API 以外のページアクセスのうち、isCreatablePage を満たすパスのみ匿名化。permalink(ObjectId)は素通し、user ページはユーザー名と残りパスを別々にハッシュ。 |
| Requirements | 6.4 |
Behavior:
/、静的リソース(/static/, /_next/, /favicon, /assets/, 拡張子付き)、/user(users top page)、permalink を除外。/user/<name>/...) はユーザー名と残りパスを SHA-256 prefix(16 文字)で別々にハッシュ → /user/[USERNAME_HASHED:<hash>][/?][HASHED:<hash>]。[HASHED:<hash>]。| Field | Detail |
|---|---|
| Intent | クエリパラメータの値を [ANONYMIZED] リテラルに置換する純粋関数 |
| Requirements | 6.2, 6.3 |
export function anonymizeQueryParams(target: string, paramNames: string[]): string;
Behavior:
[ANONYMIZED] で置換。値が JSON 配列フォーマットなら ["[ANONYMIZED]"] を返す。paramName[] 形式の配列パラメータには [ANONYMIZED] を 1 つだけ残す。| Field | Detail |
|---|---|
| Intent | OpenTelemetry incubating semconv を文字列定数として固定化 |
| Requirements | 9.1, 9.2 |
export const ATTR_SERVICE_INSTANCE_ID = 'service.instance.id';
export const ATTR_HTTP_TARGET = 'http.target';
Implementation Notes
各レイヤで発生する例外は その場で吸収し、上位レイヤや他メトリクスを巻き込まない ことを基本方針とする。
growiInfoService 失敗時は try/catch で空 Attributes を返す(SDK 起動は継続可能)。addBatchObservableCallback 内で try/catch。例外時は diag.createComponentLogger(...).error(...) を呼び、その collection cycle では observe を 1 回も呼ばない。次の cycle で再試行。handle / canHandle で try/catch、失敗時は anonymization をスキップし元の URL のまま span 属性に乗せる(または何もしない)。| 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。 |
growi:custom-metrics:applicationgrowi:custom-metrics:user-countsgrowi:custom-metrics:page-countsgrowi:custom-metrics:systemgrowi:anonymization:<handler-name>loggerFactory('growi:opentelemetry:<sub-namespace>') で起動完了メッセージを info ログ出力。各モジュールに *.spec.ts を co-locate する。テスト設計の指針:
os-resource-attributes.spec.ts, application-resource-attributes.spec.ts): vi.mock('node:os') で stdlib を、vi.mock で growiInfoService をモックし、戻り値 attributes のキー集合を assert。vi.mock('@opentelemetry/api') で metrics, diag をモック、vitest-mock-extended の mock<Meter>() / mock<ObservableGauge>() で gauge を取得し、meter.addBatchObservableCallback.mock.calls[0][0] でコールバックを取り出して直接実行する。result.observe のモックを assert する。canHandle の境界条件(先頭一致 / クエリ有無 / 静的リソース除外 / permalink 除外)と handle の URL 変換を網羅。anonymize-query-params.spec.ts で JSON 配列 / paramName[] フォーマットを網羅。vi.mock('node:os'), vi.mock('node:v8'), vi.spyOn(process, 'constrainedMemory'), vi.spyOn(process, 'memoryUsage') を組み合わせる。node-sdk.spec.ts が SDK 構築・初期化シーケンスを統合的に検証。node-sdk.testing.ts がテストヘルパとして共通利用される。OTEL_EXPORTER_OTLP_ENDPOINT を http://localhost:4317 等に向け、Collector の receiver ログで以下を確認:
growi.configs の attachment_type ラベルが期待値(aws / gcs / gridfs / local / mongodb / azure 等)または空文字であること。system.host.memory.total / process.memory.usage / process.runtime.v8.heap.* が約 5 分間隔で届くこと。--memory=512m を指定した場合に system.memory.limit が約 536870912、未指定時は emit されないこと。OPENTELEMETRY_ANONYMIZE_IN_BEST_EFFORT=true のとき、検索 / 編集等の操作後 span の http.target が [ANONYMIZED] / [HASHED:...] で置換されていること。