Przeglądaj źródła

WIP: custom attributes and metrics

Yuki Takei 9 miesięcy temu
rodzic
commit
a7b39e533e

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

@@ -0,0 +1,108 @@
+# OpenTelemetry Custom Metrics Architecture
+
+## 概要
+
+GROWIのOpenTelemetryカスタムメトリクスは、以下の3つのカテゴリに分類して実装されています:
+
+1. **Resource Attributes** - システム起動時に設定される静的情報
+2. **Info Metrics** - 設定変更により動的に変わる可能性があるメタデータ
+3. **Custom Metrics** - 時間と共に変化する業務メトリクス
+
+## アーキテクチャ
+
+### Resource Attributes
+
+静的なシステム情報をOpenTelemetryのResource Attributesとして設定します。これらの値はアプリケーション起動時に一度だけ設定され、運用中は変更されません。
+
+#### 実装場所
+```
+src/features/opentelemetry/server/custom-resource-attributes/
+├── os-resource-attributes.ts        # OS情報
+└── application-resource-attributes.ts  # アプリケーション固定情報・インストール情報
+```
+
+#### OS情報 (`os-resource-attributes.ts`)
+- `os.type` - OS種別 (Linux, Windows等)
+- `os.platform` - プラットフォーム (linux, darwin等)
+- `os.arch` - アーキテクチャ (x64, arm64等)
+- `os.totalmem` - 総メモリ量
+
+#### アプリケーション固定情報 (`application-resource-attributes.ts`)
+- `growi.service.type` - サービスタイプ
+- `growi.deployment.type` - デプロイメントタイプ
+- `growi.attachment.type` - ファイルアップロードタイプ
+- `growi.installed.at` - インストール日時
+- `growi.installed.by_oldest_user` - 最古ユーザー作成日時
+
+### Info Metrics
+
+設定変更により動的に変わる可能性があるメタデータをInfo 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  # Info Metrics (既存)
+└── user-counts-metrics.ts  # ユーザー数メトリクス (新規作成)
+```
+
+#### ユーザー数メトリクス (`user-counts-metrics.ts`)
+- `growi.users.total` - 総ユーザー数
+- `growi.users.active` - アクティブユーザー数
+
+## 収集間隔
+
+- **Resource Attributes**: アプリケーション起動時に1回のみ設定
+- **Info Metrics**: 60秒間隔で収集 (デフォルト)
+- **Custom Metrics**: 60秒間隔で収集 (デフォルト)
+
+## 設定の変更
+
+メトリクス収集間隔は `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種別の分布円グラフ
+- 外部認証プロバイダーの利用状況

+ 79 - 0
apps/app/src/features/opentelemetry/docs/implementation-guide.md

@@ -0,0 +1,79 @@
+# 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. Info Metrics
+- **実装場所**: `src/features/opentelemetry/server/custom-metrics/application-metrics.ts`
+- **メトリクス**: `growi.info` (値は常に1、情報はラベルに格納)
+- **収集情報**: サービスインスタンスID、サイトURL、Wiki種別、外部認証タイプ
+
+#### 3. Custom Metrics
+- **実装場所**: `src/features/opentelemetry/server/custom-metrics/user-counts-metrics.ts`
+- **メトリクス**: 
+  - `growi.users.total` - 総ユーザー数
+  - `growi.users.active` - アクティブユーザー数
+
+### 📋 次のステップ
+
+#### Resource Attributesの統合
+1. `node-sdk-configuration.ts` でResource Attributesを統合する
+2. 既存のResource設定に新しいAttributesを追加する
+
+```typescript
+// 統合例
+import { getOsResourceAttributes, getApplicationResourceAttributes } from './custom-resource-attributes';
+
+const osAttributes = getOsResourceAttributes();
+const appAttributes = await getApplicationResourceAttributes();
+
+resource = resourceFromAttributes({
+  [ATTR_SERVICE_NAME]: 'growi',
+  [ATTR_SERVICE_VERSION]: version,
+  ...osAttributes,
+  ...appAttributes,
+});
+```
+
+#### メトリクス収集の統合
+1. 既存のメトリクス初期化処理にユーザー数メトリクスを追加する
+
+```typescript
+// 統合例
+import { addApplicationMetrics } from './custom-metrics/application-metrics';
+import { addUserCountsMetrics } from './custom-metrics/user-counts-metrics';
+
+// メトリクス初期化時に両方を呼び出す
+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             # Info Metrics (更新済み)
+│   └── user-counts-metrics.ts             # ユーザー数メトリクス (新規)
+└── docs/
+    ├── custom-metrics-architecture.md     # アーキテクチャ文書
+    └── implementation-guide.md            # このファイル
+```
+
+## 設計のポイント
+
+1. **循環依存の回避**: 動的importを使用してgrowiInfoServiceを読み込み
+2. **エラーハンドリング**: 各メトリクス収集でtry-catchを実装
+3. **型安全性**: Optional chainingを使用してundefinedを適切に処理
+4. **ログ出力**: デバッグ用のログを各段階で出力

+ 22 - 13
apps/app/src/features/opentelemetry/server/custom-metrics/application-metrics.ts

@@ -7,31 +7,40 @@ const loggerDiag = diag.createComponentLogger({ namespace: 'growi:custom-metrics
 
 
 
 
 export function addApplicationMetrics(): void {
 export function addApplicationMetrics(): void {
-  logger.info('Starting application metrics collection');
+  logger.info('Starting application info metrics collection');
 
 
   const meter = metrics.getMeter('growi-application-metrics', '1.0.0');
   const meter = metrics.getMeter('growi-application-metrics', '1.0.0');
 
 
-  // Dummy metrics (for future application-specific metrics)
-  const dummyGauge = meter.createObservableGauge('growi.app.dummy.metric', {
-    description: 'Dummy metric for application metrics (placeholder)',
-    unit: 'count',
+  // Info metrics: GROWI instance information (Prometheus info pattern)
+  const growiInfoGauge = meter.createObservableGauge('growi.info', {
+    description: 'GROWI instance information (always 1)',
+    unit: '1',
   });
   });
 
 
-  // Metrics collection callback
+  // Info metrics collection callback
   meter.addBatchObservableCallback(
   meter.addBatchObservableCallback(
-    (result) => {
+    async(result) => {
       try {
       try {
-        // Currently sending dummy values (actual metrics to be implemented later)
-        result.observe(dummyGauge, 1, {
-          'app.name': 'growi',
+        // Dynamic import to avoid circular dependencies
+        const { growiInfoService } = await import('~/server/service/growi-info');
+
+        const growiInfo = await growiInfoService.getGrowiInfo(true);
+
+        // Info metrics always have value 1, with information stored in labels
+        result.observe(growiInfoGauge, 1, {
+          // Dynamic information that can change through configuration
+          service_instance_id: growiInfo.serviceInstanceId || '',
+          site_url: growiInfo.appSiteUrl,
+          wiki_type: growiInfo.wikiType,
+          external_auth_types: growiInfo.additionalInfo?.activeExternalAccountTypes?.join(',') || '',
         });
         });
       }
       }
       catch (error) {
       catch (error) {
-        loggerDiag.error('Failed to collect application metrics', { error });
+        loggerDiag.error('Failed to collect application info metrics', { error });
       }
       }
     },
     },
-    [dummyGauge],
+    [growiInfoGauge],
   );
   );
 
 
-  logger.info('Application metrics collection started successfully');
+  logger.info('Application info metrics collection started successfully');
 }
 }

+ 46 - 0
apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.ts

@@ -0,0 +1,46 @@
+import { diag, metrics } from '@opentelemetry/api';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:custom-metrics:user-counts');
+const loggerDiag = diag.createComponentLogger({ namespace: 'growi:custom-metrics:user-counts' });
+
+export function addUserCountsMetrics(): void {
+  logger.info('Starting user counts metrics collection');
+
+  const meter = metrics.getMeter('growi-user-counts-metrics', '1.0.0');
+
+  // Total user count gauge
+  const userCountGauge = meter.createObservableGauge('growi.users.total', {
+    description: 'Total number of users in GROWI',
+    unit: 'users',
+  });
+
+  // Active user count gauge
+  const activeUserCountGauge = meter.createObservableGauge('growi.users.active', {
+    description: 'Number of active users in GROWI',
+    unit: 'users',
+  });
+
+  // User metrics collection callback
+  meter.addBatchObservableCallback(
+    async(result) => {
+      try {
+        // Dynamic import to avoid circular dependencies
+        const { growiInfoService } = await import('~/server/service/growi-info');
+
+        const growiInfo = await growiInfoService.getGrowiInfo(true);
+
+        // Observe user count metrics
+        result.observe(userCountGauge, growiInfo.additionalInfo?.currentUsersCount || 0);
+        result.observe(activeUserCountGauge, growiInfo.additionalInfo?.currentActiveUsersCount || 0);
+      }
+      catch (error) {
+        loggerDiag.error('Failed to collect user counts metrics', { error });
+      }
+    },
+    [userCountGauge, activeUserCountGauge],
+  );
+
+  logger.info('User counts metrics collection started successfully');
+}

+ 39 - 0
apps/app/src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.ts

@@ -0,0 +1,39 @@
+import type { Attributes } from '@opentelemetry/api';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:custom-resource-attributes:application');
+
+/**
+ * Get application fixed information as OpenTelemetry Resource Attributes
+ * These attributes are static and set once during application startup
+ */
+export async function getApplicationResourceAttributes(): Promise<Attributes> {
+  logger.info('Collecting application resource attributes');
+
+  try {
+    // Dynamic import to avoid circular dependencies
+    const { growiInfoService } = await import('~/server/service/growi-info');
+
+    const growiInfo = await growiInfoService.getGrowiInfo(true);
+
+    const attributes: Attributes = {
+      // Service configuration (rarely changes after system setup)
+      'growi.service.type': growiInfo.type,
+      'growi.deployment.type': growiInfo.deploymentType,
+      'growi.attachment.type': growiInfo.additionalInfo?.attachmentType,
+
+      // Installation information (fixed values)
+      'growi.installedAt': growiInfo.additionalInfo?.installedAt?.toISOString(),
+      'growi.installedAt.by_oldest_user': growiInfo.additionalInfo?.installedAtByOldestUser?.toISOString(),
+    };
+
+    logger.info('Application resource attributes collected', { attributes });
+
+    return attributes;
+  }
+  catch (error) {
+    logger.error('Failed to collect application resource attributes', { error });
+    return {};
+  }
+}

+ 2 - 0
apps/app/src/features/opentelemetry/server/custom-resource-attributes/index.ts

@@ -0,0 +1,2 @@
+export { getOsResourceAttributes } from './os-resource-attributes';
+export { getApplicationResourceAttributes } from './application-resource-attributes';

+ 33 - 0
apps/app/src/features/opentelemetry/server/custom-resource-attributes/os-resource-attributes.ts

@@ -0,0 +1,33 @@
+import * as os from 'node:os';
+
+import type { Attributes } from '@opentelemetry/api';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:custom-resource-attributes:os');
+
+/**
+ * Get OS information as OpenTelemetry Resource Attributes
+ * These attributes are static and set once during application startup
+ */
+export function getOsResourceAttributes(): Attributes {
+  logger.info('Collecting OS resource attributes');
+
+  const osInfo = {
+    type: os.type(),
+    platform: os.platform(),
+    arch: os.arch(),
+    totalmem: os.totalmem(),
+  };
+
+  const attributes: Attributes = {
+    'os.type': osInfo.type,
+    'os.platform': osInfo.platform,
+    'os.arch': osInfo.arch,
+    'os.totalmem': osInfo.totalmem,
+  };
+
+  logger.info('OS resource attributes collected', { attributes });
+
+  return attributes;
+}