Răsfoiți Sursa

add page counnts metrics

ryosei-f 6 luni în urmă
părinte
comite
ad6969cbb9

+ 3 - 0
apps/app/src/features/opentelemetry/server/custom-metrics/index.ts

@@ -1,11 +1,14 @@
 export { addApplicationMetrics } from './application-metrics';
+export { addPageCountsMetrics } from './page-counts-metrics';
 export { addUserCountsMetrics } from './user-counts-metrics';
 
 export const setupCustomMetrics = async (): Promise<void> => {
   const { addApplicationMetrics } = await import('./application-metrics');
   const { addUserCountsMetrics } = await import('./user-counts-metrics');
+  const { addPageCountsMetrics } = await import('./page-counts-metrics');
 
   // Add custom metrics
   addApplicationMetrics();
   addUserCountsMetrics();
+  addPageCountsMetrics();
 };

+ 147 - 0
apps/app/src/features/opentelemetry/server/custom-metrics/page-counts-metrics.spec.ts

@@ -0,0 +1,147 @@
+import { type Meter, metrics, type ObservableGauge } from '@opentelemetry/api';
+import { mock } from 'vitest-mock-extended';
+
+import { addPageCountsMetrics } from './page-counts-metrics';
+
+// Mock external dependencies
+vi.mock('~/utils/logger', () => ({
+  default: () => ({
+    info: vi.fn(),
+  }),
+}));
+
+vi.mock('@opentelemetry/api', () => ({
+  diag: {
+    createComponentLogger: () => ({
+      error: vi.fn(),
+    }),
+  },
+  metrics: {
+    getMeter: vi.fn(),
+  },
+}));
+
+// Mock growi-info service
+const mockGrowiInfoService = {
+  getGrowiInfo: vi.fn(),
+};
+vi.mock('~/server/service/growi-info', async () => ({
+  growiInfoService: mockGrowiInfoService,
+}));
+
+describe('addPageCountsMetrics', () => {
+  const mockMeter = mock<Meter>();
+  const mockPageCountGauge = mock<ObservableGauge>();
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(metrics.getMeter).mockReturnValue(mockMeter);
+    mockMeter.createObservableGauge.mockReturnValueOnce(mockPageCountGauge);
+  });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });
+
+  it('should create observable gauges and set up metrics collection', () => {
+    addPageCountsMetrics();
+
+    expect(metrics.getMeter).toHaveBeenCalledWith(
+      'growi-page-counts-metrics',
+      '1.0.0',
+    );
+    expect(mockMeter.createObservableGauge).toHaveBeenCalledWith(
+      'growi.pages.total',
+      {
+        description: 'Total number of pages in GROWI',
+        unit: 'pages',
+      },
+    );
+    expect(mockMeter.addBatchObservableCallback).toHaveBeenCalledWith(
+      expect.any(Function),
+      [mockPageCountGauge],
+    );
+  });
+
+  describe('metrics callback behavior', () => {
+    const mockGrowiInfo = {
+      additionalInfo: {
+        currentPagesCount: 1234,
+      },
+    };
+
+    beforeEach(() => {
+      mockGrowiInfoService.getGrowiInfo.mockResolvedValue(mockGrowiInfo);
+    });
+
+    it('should observe page count metrics when growi info is available', async () => {
+      const mockResult = { observe: vi.fn() };
+
+      addPageCountsMetrics();
+
+      // Get the callback function that was passed to addBatchObservableCallback
+      const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
+      await callback(mockResult);
+
+      expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith({
+        includePageCountInfo: true,
+      });
+      expect(mockResult.observe).toHaveBeenCalledWith(mockPageCountGauge, 1234);
+    });
+
+    it('should use default values when page counts are missing', async () => {
+      const mockResult = { observe: vi.fn() };
+
+      const growiInfoWithoutCounts = {
+        additionalInfo: {
+          // Missing currentPagesCount
+        },
+      };
+      mockGrowiInfoService.getGrowiInfo.mockResolvedValue(
+        growiInfoWithoutCounts,
+      );
+
+      addPageCountsMetrics();
+
+      const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
+      await callback(mockResult);
+
+      expect(mockResult.observe).toHaveBeenCalledWith(mockPageCountGauge, 0);
+    });
+
+    it('should handle missing additionalInfo gracefully', async () => {
+      const mockResult = { observe: vi.fn() };
+
+      const growiInfoWithoutAdditionalInfo = {
+        // Missing additionalInfo entirely
+      };
+      mockGrowiInfoService.getGrowiInfo.mockResolvedValue(
+        growiInfoWithoutAdditionalInfo,
+      );
+
+      addPageCountsMetrics();
+
+      const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
+      await callback(mockResult);
+
+      expect(mockResult.observe).toHaveBeenCalledWith(mockPageCountGauge, 0);
+    });
+
+    it('should handle errors in metrics collection gracefully', async () => {
+      mockGrowiInfoService.getGrowiInfo.mockRejectedValue(
+        new Error('Service unavailable'),
+      );
+      const mockResult = { observe: vi.fn() };
+
+      addPageCountsMetrics();
+
+      const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
+
+      // Should not throw error
+      await expect(callback(mockResult)).resolves.toBeUndefined();
+
+      // Should not call observe when error occurs
+      expect(mockResult.observe).not.toHaveBeenCalled();
+    });
+  });
+});

+ 42 - 0
apps/app/src/features/opentelemetry/server/custom-metrics/page-counts-metrics.ts

@@ -0,0 +1,42 @@
+import { diag, metrics } from '@opentelemetry/api';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:custom-metrics:page-counts');
+const loggerDiag = diag.createComponentLogger({
+  namespace: 'growi:custom-metrics:page-counts',
+});
+
+export function addPageCountsMetrics(): void {
+  logger.info('Starting page counts metrics collection');
+
+  const meter = metrics.getMeter('growi-page-counts-metrics', '1.0.0');
+
+  const pageCountGauge = meter.createObservableGauge('growi.pages.total', {
+    description: 'Total number of pages in GROWI',
+    unit: 'pages',
+  });
+
+  meter.addBatchObservableCallback(
+    async (result) => {
+      try {
+        const { growiInfoService } = await import(
+          '~/server/service/growi-info'
+        );
+
+        const growiInfo = await growiInfoService.getGrowiInfo({
+          includePageCountInfo: true,
+        });
+
+        result.observe(
+          pageCountGauge,
+          growiInfo.additionalInfo?.currentPagesCount || 0,
+        );
+      } catch (error) {
+        loggerDiag.error('Failed to collect page counts metrics', { error });
+      }
+    },
+    [pageCountGauge],
+  );
+  logger.info('Page counts metrics collection started successfully');
+}

+ 5 - 0
apps/app/src/server/service/growi-info/growi-info.integ.ts

@@ -109,6 +109,7 @@ describe('GrowiInfoService', () => {
           currentActiveUsersCount: 1,
           attachmentType: 'aws',
           activeExternalAccountTypes: ['saml', 'github'],
+          currentPagesCount: 0,
         },
       });
     });
@@ -158,6 +159,7 @@ describe('GrowiInfoService', () => {
       const growiInfo = await growiInfoService.getGrowiInfo({
         includeAttachmentInfo: true,
         includeUserCountInfo: true,
+        includePageCountInfo: true,
       });
 
       // assert
@@ -167,6 +169,7 @@ describe('GrowiInfoService', () => {
         activeExternalAccountTypes: ['saml', 'github'],
         currentUsersCount: 1,
         currentActiveUsersCount: 1,
+        currentPagesCount: 0,
       });
     });
 
@@ -176,6 +179,7 @@ describe('GrowiInfoService', () => {
         includeAttachmentInfo: true,
         includeInstalledInfo: true,
         includeUserCountInfo: true,
+        includePageCountInfo: true,
       });
 
       // assert
@@ -187,6 +191,7 @@ describe('GrowiInfoService', () => {
         installedAtByOldestUser: new Date('2000-01-01'),
         currentUsersCount: 1,
         currentActiveUsersCount: 1,
+        currentPagesCount: 0,
       });
     });
 

+ 10 - 2
apps/app/src/server/service/growi-info/growi-info.ts

@@ -13,6 +13,7 @@ import mongoose from 'mongoose';
 
 import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { Config } from '~/server/models/config';
+import createPageModel from '~/server/models/page';
 import { aclService } from '~/server/service/acl';
 import { configManager } from '~/server/service/config-manager';
 import { getGrowiVersion } from '~/utils/growi-version';
@@ -22,6 +23,7 @@ const FULL_ADDITIONAL_INFO_OPTIONS = {
   includeAttachmentInfo: true,
   includeInstalledInfo: true,
   includeUserCountInfo: true,
+  includePageCountInfo: true,
 } as const;
 
 
@@ -116,9 +118,9 @@ export class GrowiInfoService {
 
   private async getAdditionalInfoByOptions<T extends GrowiInfoOptions>(options: T): Promise<IGrowiAdditionalInfoResult<T>> {
     const User = mongoose.model<IUser, Model<IUser>>('User');
-
+    const Page = createPageModel(null);
     // Check if any option is enabled to determine if we should return additional info
-    const hasAnyOption = options.includeAttachmentInfo || options.includeInstalledInfo || options.includeUserCountInfo;
+    const hasAnyOption = options.includeAttachmentInfo || options.includeInstalledInfo || options.includeUserCountInfo || options.includePageCountInfo;
 
     if (!hasAnyOption) {
       return undefined as IGrowiAdditionalInfoResult<T>;
@@ -137,6 +139,7 @@ export class GrowiInfoService {
       installedAtByOldestUser: Date | null;
       currentUsersCount: number;
       currentActiveUsersCount: number;
+      currentPagesCount: number;
     }> = {
       attachmentType: configManager.getConfig('app:fileUploadType'),
       activeExternalAccountTypes,
@@ -163,6 +166,11 @@ export class GrowiInfoService {
       partialResult.currentActiveUsersCount = currentActiveUsersCount;
     }
 
+    if (options.includePageCountInfo) {
+      const currentPagesCount = await Page.countDocuments();
+      partialResult.currentPagesCount = currentPagesCount;
+    }
+
     return partialResult as IGrowiAdditionalInfoResult<T>;
   }
 

+ 12 - 2
packages/core/src/interfaces/growi-app-info.ts

@@ -10,8 +10,8 @@ export interface GrowiInfoOptions {
   includeAttachmentInfo?: boolean;
   includeInstalledInfo?: boolean;
   includeUserCountInfo?: boolean;
+  includePageCountInfo?: boolean;
   // Future extensions can be added here
-  // includePageCount?: boolean;
 }
 
 interface IGrowiOSInfo {
@@ -36,9 +36,14 @@ interface IAdditionalUserCountInfo {
   currentActiveUsersCount: number;
 }
 
+interface IAdditionalPageCountInfo {
+  currentPagesCount: number;
+}
+
 export interface IGrowiAdditionalInfo
   extends IAdditionalInstalledAtInfo,
     IAdditionalUserCountInfo,
+    IAdditionalPageCountInfo,
     IAdditionalAttachmentInfo {}
 
 // Type mapping for flexible options
@@ -51,6 +56,9 @@ export type IGrowiAdditionalInfoByOptions<T extends GrowiInfoOptions> =
       : Record<string, never>) &
     (T['includeUserCountInfo'] extends true
       ? IAdditionalUserCountInfo
+      : Record<string, never>) &
+    (T['includePageCountInfo'] extends true
+      ? IAdditionalPageCountInfo
       : Record<string, never>);
 
 // Helper type to check if any option is enabled
@@ -61,7 +69,9 @@ export type HasAnyOption<T extends GrowiInfoOptions> =
       ? true
       : T['includeUserCountInfo'] extends true
         ? true
-        : false;
+        : T['includePageCountInfo'] extends true
+          ? true
+          : false;
 
 // Final result type based on options
 export type IGrowiAdditionalInfoResult<T extends GrowiInfoOptions> =