Просмотр исходного кода

Merge pull request #10189 from weseek/imprv/export-interval-for-otel-metrics

imprv: Set a longer value for the export interval of OpenTelemetry metrics
mergify[bot] 8 месяцев назад
Родитель
Сommit
18e2a9b570

+ 5 - 0
.changeset/fluffy-insects-laugh.md

@@ -0,0 +1,5 @@
+---
+'@growi/core': minor
+---
+
+Update IGrowiInfo type

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

@@ -32,7 +32,7 @@ export function addApplicationMetrics(): void {
       try {
       try {
         // Dynamic import to avoid circular dependencies
         // Dynamic import to avoid circular dependencies
         const { growiInfoService } = await import('~/server/service/growi-info');
         const { growiInfoService } = await import('~/server/service/growi-info');
-        const growiInfo = await growiInfoService.getGrowiInfo(true);
+        const growiInfo = await growiInfoService.getGrowiInfo({ includeAttachmentInfo: true });
 
 
         const isAppSiteUrlHashed = configManager.getConfig('otel:isAppSiteUrlHashed');
         const isAppSiteUrlHashed = configManager.getConfig('otel:isAppSiteUrlHashed');
 
 

+ 2 - 2
apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.spec.ts

@@ -25,7 +25,7 @@ vi.mock('@opentelemetry/api', () => ({
 const mockGrowiInfoService = {
 const mockGrowiInfoService = {
   getGrowiInfo: vi.fn(),
   getGrowiInfo: vi.fn(),
 };
 };
-vi.mock('~/server/service/growi-info', () => ({
+vi.mock('~/server/service/growi-info', async() => ({
   growiInfoService: mockGrowiInfoService,
   growiInfoService: mockGrowiInfoService,
 }));
 }));
 
 
@@ -85,7 +85,7 @@ describe('addUserCountsMetrics', () => {
       const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
       const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
       await callback(mockResult);
       await callback(mockResult);
 
 
-      expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith(true);
+      expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith({ includeUserCountInfo: true });
       expect(mockResult.observe).toHaveBeenCalledWith(mockUserCountGauge, 150);
       expect(mockResult.observe).toHaveBeenCalledWith(mockUserCountGauge, 150);
       expect(mockResult.observe).toHaveBeenCalledWith(mockActiveUserCountGauge, 75);
       expect(mockResult.observe).toHaveBeenCalledWith(mockActiveUserCountGauge, 75);
     });
     });

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

@@ -29,7 +29,7 @@ export function addUserCountsMetrics(): void {
         // Dynamic import to avoid circular dependencies
         // Dynamic import to avoid circular dependencies
         const { growiInfoService } = await import('~/server/service/growi-info');
         const { growiInfoService } = await import('~/server/service/growi-info');
 
 
-        const growiInfo = await growiInfoService.getGrowiInfo(true);
+        const growiInfo = await growiInfoService.getGrowiInfo({ includeUserCountInfo: true });
 
 
         // Observe user count metrics
         // Observe user count metrics
         result.observe(userCountGauge, growiInfo.additionalInfo?.currentUsersCount || 0);
         result.observe(userCountGauge, growiInfo.additionalInfo?.currentUsersCount || 0);

+ 1 - 1
apps/app/src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.spec.ts

@@ -43,7 +43,7 @@ describe('getApplicationResourceAttributes', () => {
       'growi.installedAt': '2023-01-01T00:00:00.000Z',
       'growi.installedAt': '2023-01-01T00:00:00.000Z',
       'growi.installedAt.by_oldest_user': '2023-01-01T00:00:00.000Z',
       'growi.installedAt.by_oldest_user': '2023-01-01T00:00:00.000Z',
     });
     });
-    expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith(true);
+    expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith({ includeInstalledInfo: true });
   });
   });
 
 
   it('should handle missing additionalInfo gracefully', async() => {
   it('should handle missing additionalInfo gracefully', async() => {

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

@@ -15,7 +15,7 @@ export async function getApplicationResourceAttributes(): Promise<Attributes> {
     // Dynamic import to avoid circular dependencies
     // Dynamic import to avoid circular dependencies
     const { growiInfoService } = await import('~/server/service/growi-info');
     const { growiInfoService } = await import('~/server/service/growi-info');
 
 
-    const growiInfo = await growiInfoService.getGrowiInfo(true);
+    const growiInfo = await growiInfoService.getGrowiInfo({ includeInstalledInfo: true });
 
 
     const attributes: Attributes = {
     const attributes: Attributes = {
       // Service configuration (rarely changes after system setup)
       // Service configuration (rarely changes after system setup)

+ 1 - 0
apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts

@@ -41,6 +41,7 @@ export const generateNodeSDKConfiguration = (opts?: Option): Configuration => {
       traceExporter: new OTLPTraceExporter(),
       traceExporter: new OTLPTraceExporter(),
       metricReader: new PeriodicExportingMetricReader({
       metricReader: new PeriodicExportingMetricReader({
         exporter: new OTLPMetricExporter(),
         exporter: new OTLPMetricExporter(),
+        exportIntervalMillis: 300000, // 5 minute
       }),
       }),
       instrumentations: [getNodeAutoInstrumentations({
       instrumentations: [getNodeAutoInstrumentations({
         '@opentelemetry/instrumentation-bunyan': {
         '@opentelemetry/instrumentation-bunyan': {

+ 0 - 18
apps/app/src/features/questionnaire/interfaces/growi-app-info.ts

@@ -1,18 +0,0 @@
-import type { IGrowiAdditionalInfo, IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { AttachmentMethodType } from '~/interfaces/attachment';
-import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
-
-
-export type IGrowiAppAdditionalInfo = IGrowiAdditionalInfo & {
-  attachmentType: AttachmentMethodType
-  activeExternalAccountTypes?: IExternalAuthProviderType[]
-}
-
-// legacy properties (extracted from additionalInfo for growi-questionnaire)
-// see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
-export type IGrowiAppInfoLegacy = Omit<IGrowiInfo<IGrowiAppAdditionalInfo>, 'additionalInfo'>
-  & IGrowiAppAdditionalInfo
-  & {
-    appSiteUrlHashed: string,
-  };

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

@@ -113,5 +113,100 @@ describe('GrowiInfoService', () => {
       });
       });
     });
     });
 
 
+    test('Should get correct GROWI info with specific options - attachment only', async() => {
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo({ includeAttachmentInfo: true });
+
+      // assert
+      assert(growiInfo != null);
+      expect(growiInfo.additionalInfo).toEqual({
+        attachmentType: 'aws',
+        activeExternalAccountTypes: ['saml', 'github'],
+      });
+    });
+
+    test('Should get correct GROWI info with specific options - user count only', async() => {
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo({ includeUserCountInfo: true });
+
+      // assert
+      assert(growiInfo != null);
+      expect(growiInfo.additionalInfo).toEqual({
+        attachmentType: 'aws',
+        activeExternalAccountTypes: ['saml', 'github'],
+        currentUsersCount: 1, // Only one user from the legacy test
+        currentActiveUsersCount: 1,
+      });
+    });
+
+    test('Should get correct GROWI info with specific options - installed info only', async() => {
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo({ includeInstalledInfo: true });
+
+      // assert
+      assert(growiInfo != null);
+      expect(growiInfo.additionalInfo).toEqual({
+        attachmentType: 'aws',
+        activeExternalAccountTypes: ['saml', 'github'],
+        installedAt: new Date('2000-01-01'),
+        installedAtByOldestUser: new Date('2000-01-01'),
+      });
+    });
+
+    test('Should get correct GROWI info with combined options', async() => {
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo({
+        includeAttachmentInfo: true,
+        includeUserCountInfo: true,
+      });
+
+      // assert
+      assert(growiInfo != null);
+      expect(growiInfo.additionalInfo).toEqual({
+        attachmentType: 'aws',
+        activeExternalAccountTypes: ['saml', 'github'],
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+      });
+    });
+
+    test('Should get correct GROWI info with all options', async() => {
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo({
+        includeAttachmentInfo: true,
+        includeInstalledInfo: true,
+        includeUserCountInfo: true,
+      });
+
+      // assert
+      assert(growiInfo != null);
+      expect(growiInfo.additionalInfo).toEqual({
+        attachmentType: 'aws',
+        activeExternalAccountTypes: ['saml', 'github'],
+        installedAt: new Date('2000-01-01'),
+        installedAtByOldestUser: new Date('2000-01-01'),
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+      });
+    });
+
+    test('Should get correct GROWI info with empty options', async() => {
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo({});
+
+      // assert
+      assert(growiInfo != null);
+      expect(growiInfo.additionalInfo).toBeUndefined();
+      expect(growiInfo).toEqual({
+        version: appVersion,
+        appSiteUrl: 'http://growi.test.jp',
+        serviceInstanceId: '',
+        type: 'on-premise',
+        wikiType: 'closed',
+        deploymentType: 'growi-docker-compose',
+        osInfo: growiInfo.osInfo, // Keep the osInfo as it's dynamic
+      });
+    });
+
   });
   });
 });
 });

+ 85 - 25
apps/app/src/server/service/growi-info/growi-info.ts

@@ -1,6 +1,10 @@
 import * as os from 'node:os';
 import * as os from 'node:os';
 
 
-import type { IGrowiInfo } from '@growi/core';
+import type {
+  IGrowiInfo,
+  GrowiInfoOptions,
+  IGrowiAdditionalInfoResult,
+} from '@growi/core';
 import type { IUser } from '@growi/core/dist/interfaces';
 import type { IUser } from '@growi/core/dist/interfaces';
 import { GrowiWikiType } from '@growi/core/dist/interfaces';
 import { GrowiWikiType } from '@growi/core/dist/interfaces';
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
@@ -13,7 +17,12 @@ import { aclService } from '~/server/service/acl';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import { getGrowiVersion } from '~/utils/growi-version';
 import { getGrowiVersion } from '~/utils/growi-version';
 
 
-import type { IGrowiAppAdditionalInfo } from '../../../features/questionnaire/interfaces/growi-app-info';
+// Local preset for full additional info
+const FULL_ADDITIONAL_INFO_OPTIONS = {
+  includeAttachmentInfo: true,
+  includeInstalledInfo: true,
+  includeUserCountInfo: true,
+} as const;
 
 
 
 
 export class GrowiInfoService {
 export class GrowiInfoService {
@@ -38,15 +47,24 @@ export class GrowiInfoService {
   /**
   /**
    * Get GROWI information
    * Get GROWI information
    */
    */
-  getGrowiInfo(): Promise<IGrowiInfo<Record<string, never>>>;
+  getGrowiInfo(): Promise<IGrowiInfo<undefined>>;
 
 
   /**
   /**
-   * Get GROWI information with additional information
+   * Get GROWI information with flexible options
+   * @param options options to determine what additional information to include
+   */
+  getGrowiInfo<T extends GrowiInfoOptions>(options: T): Promise<IGrowiInfo<IGrowiAdditionalInfoResult<T>>>;
+
+  /**
+   * Get GROWI information with additional information (legacy)
    * @param includeAdditionalInfo whether to include additional information
    * @param includeAdditionalInfo whether to include additional information
+   * @deprecated Use getGrowiInfo(options) instead
    */
    */
-  getGrowiInfo(includeAdditionalInfo: true): Promise<IGrowiInfo<IGrowiAppAdditionalInfo>>;
+  getGrowiInfo(includeAdditionalInfo: true): Promise<IGrowiInfo<IGrowiAdditionalInfoResult<typeof FULL_ADDITIONAL_INFO_OPTIONS>>>;
 
 
-  async getGrowiInfo(includeAdditionalInfo?: boolean): Promise<IGrowiInfo<Record<string, never>> | IGrowiInfo<IGrowiAppAdditionalInfo>> {
+  async getGrowiInfo<T extends GrowiInfoOptions>(
+      optionsOrLegacyFlag?: T | true,
+  ): Promise<IGrowiInfo<IGrowiAdditionalInfoResult<T>> | IGrowiInfo<undefined> | IGrowiInfo<IGrowiAdditionalInfoResult<typeof FULL_ADDITIONAL_INFO_OPTIONS>>> {
 
 
     const appSiteUrl = this.getSiteUrl();
     const appSiteUrl = this.getSiteUrl();
 
 
@@ -66,44 +84,86 @@ export class GrowiInfoService {
       type: configManager.getConfig('app:serviceType'),
       type: configManager.getConfig('app:serviceType'),
       wikiType,
       wikiType,
       deploymentType: configManager.getConfig('app:deploymentType'),
       deploymentType: configManager.getConfig('app:deploymentType'),
-    } satisfies IGrowiInfo<Record<string, never>>;
+    } satisfies IGrowiInfo<undefined>;
 
 
-    if (!includeAdditionalInfo) {
+    if (optionsOrLegacyFlag == null) {
       return baseInfo;
       return baseInfo;
     }
     }
 
 
+    let options: GrowiInfoOptions;
+
+    // Handle different parameter types
+    if (typeof optionsOrLegacyFlag === 'boolean') {
+      // Legacy boolean parameter
+      options = optionsOrLegacyFlag ? FULL_ADDITIONAL_INFO_OPTIONS : {};
+    }
+    else {
+      // GrowiInfoOptions parameter
+      options = optionsOrLegacyFlag;
+    }
+
+    const additionalInfo = await this.getAdditionalInfoByOptions(options);
+
+    if (!additionalInfo) {
+      return baseInfo as IGrowiInfo<IGrowiAdditionalInfoResult<T>>;
+    }
+
     return {
     return {
       ...baseInfo,
       ...baseInfo,
-      additionalInfo: await this.getAdditionalInfo(),
-    };
+      additionalInfo,
+    } as IGrowiInfo<IGrowiAdditionalInfoResult<T>>;
   }
   }
 
 
-  private async getAdditionalInfo(): Promise<IGrowiAppAdditionalInfo> {
+  private async getAdditionalInfoByOptions<T extends GrowiInfoOptions>(options: T): Promise<IGrowiAdditionalInfoResult<T>> {
     const User = mongoose.model<IUser, Model<IUser>>('User');
     const User = mongoose.model<IUser, Model<IUser>>('User');
 
 
-    // Get the oldest user who probably installed this GROWI.
-    const user = await User.findOne({ createdAt: { $ne: null } }).sort({ createdAt: 1 });
-    const installedAtByOldestUser = user ? user.createdAt : null;
+    // Check if any option is enabled to determine if we should return additional info
+    const hasAnyOption = options.includeAttachmentInfo || options.includeInstalledInfo || options.includeUserCountInfo;
 
 
-    const appInstalledConfig = await Config.findOne({ key: 'app:installed' });
-    const oldestConfig = await Config.findOne().sort({ createdAt: 1 });
-    const installedAt = installedAtByOldestUser ?? appInstalledConfig?.createdAt ?? oldestConfig!.createdAt ?? null;
-
-    const currentUsersCount = await User.countDocuments();
-    const currentActiveUsersCount = await (User as any).countActiveUsers();
+    if (!hasAnyOption) {
+      return undefined as IGrowiAdditionalInfoResult<T>;
+    }
 
 
+    // Include attachment info (required for all additional info)
     const activeExternalAccountTypes: IExternalAuthProviderType[] = Object.values(IExternalAuthProviderType).filter((type) => {
     const activeExternalAccountTypes: IExternalAuthProviderType[] = Object.values(IExternalAuthProviderType).filter((type) => {
       return configManager.getConfig(`security:passport-${type}:isEnabled`);
       return configManager.getConfig(`security:passport-${type}:isEnabled`);
     });
     });
 
 
-    return {
-      installedAt,
-      installedAtByOldestUser,
-      currentUsersCount,
-      currentActiveUsersCount,
+    // Build result incrementally with proper typing
+    const partialResult: Partial<{
+      attachmentType: unknown;
+      activeExternalAccountTypes: IExternalAuthProviderType[];
+      installedAt: Date | null;
+      installedAtByOldestUser: Date | null;
+      currentUsersCount: number;
+      currentActiveUsersCount: number;
+    }> = {
       attachmentType: configManager.getConfig('app:fileUploadType'),
       attachmentType: configManager.getConfig('app:fileUploadType'),
       activeExternalAccountTypes,
       activeExternalAccountTypes,
     };
     };
+
+    if (options.includeInstalledInfo) {
+      // Get the oldest user who probably installed this GROWI.
+      const user = await User.findOne({ createdAt: { $ne: null } }).sort({ createdAt: 1 });
+      const installedAtByOldestUser = user ? user.createdAt : null;
+
+      const appInstalledConfig = await Config.findOne({ key: 'app:installed' });
+      const oldestConfig = await Config.findOne().sort({ createdAt: 1 });
+      const installedAt = installedAtByOldestUser ?? appInstalledConfig?.createdAt ?? oldestConfig?.createdAt ?? null;
+
+      partialResult.installedAt = installedAt;
+      partialResult.installedAtByOldestUser = installedAtByOldestUser;
+    }
+
+    if (options.includeUserCountInfo) {
+      const currentUsersCount = await User.countDocuments();
+      const currentActiveUsersCount = await (User as unknown as { countActiveUsers: () => Promise<number> }).countActiveUsers();
+
+      partialResult.currentUsersCount = currentUsersCount;
+      partialResult.currentActiveUsersCount = currentActiveUsersCount;
+    }
+
+    return partialResult as IGrowiAdditionalInfoResult<T>;
   }
   }
 
 
 }
 }

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

@@ -5,6 +5,15 @@ import type { GrowiDeploymentType, GrowiServiceType } from '../consts/system';
 export const GrowiWikiType = { open: 'open', closed: 'closed' } as const;
 export const GrowiWikiType = { open: 'open', closed: 'closed' } as const;
 type GrowiWikiType = (typeof GrowiWikiType)[keyof typeof GrowiWikiType];
 type GrowiWikiType = (typeof GrowiWikiType)[keyof typeof GrowiWikiType];
 
 
+// Info options for additionalInfo selection
+export interface GrowiInfoOptions {
+  includeAttachmentInfo?: boolean;
+  includeInstalledInfo?: boolean;
+  includeUserCountInfo?: boolean;
+  // Future extensions can be added here
+  // includePageCount?: boolean;
+}
+
 interface IGrowiOSInfo {
 interface IGrowiOSInfo {
   type?: ReturnType<typeof os.type>;
   type?: ReturnType<typeof os.type>;
   platform?: ReturnType<typeof os.platform>;
   platform?: ReturnType<typeof os.platform>;
@@ -12,14 +21,53 @@ interface IGrowiOSInfo {
   totalmem?: ReturnType<typeof os.totalmem>;
   totalmem?: ReturnType<typeof os.totalmem>;
 }
 }
 
 
-export interface IGrowiAdditionalInfo {
+interface IAdditionalAttachmentInfo {
+  attachmentType: string;
+  activeExternalAccountTypes: string[];
+}
+
+interface IAdditionalInstalledAtInfo {
   installedAt: Date;
   installedAt: Date;
   installedAtByOldestUser: Date | null;
   installedAtByOldestUser: Date | null;
+}
+
+interface IAdditionalUserCountInfo {
   currentUsersCount: number;
   currentUsersCount: number;
   currentActiveUsersCount: number;
   currentActiveUsersCount: number;
 }
 }
 
 
-export interface IGrowiInfo<A extends object = IGrowiAdditionalInfo> {
+export interface IGrowiAdditionalInfo
+  extends IAdditionalInstalledAtInfo,
+    IAdditionalUserCountInfo,
+    IAdditionalAttachmentInfo {}
+
+// Type mapping for flexible options
+export type IGrowiAdditionalInfoByOptions<T extends GrowiInfoOptions> =
+  (T['includeAttachmentInfo'] extends true
+    ? IAdditionalAttachmentInfo
+    : Record<string, never>) &
+    (T['includeInstalledInfo'] extends true
+      ? IAdditionalInstalledAtInfo
+      : Record<string, never>) &
+    (T['includeUserCountInfo'] extends true
+      ? IAdditionalUserCountInfo
+      : Record<string, never>);
+
+// Helper type to check if any option is enabled
+export type HasAnyOption<T extends GrowiInfoOptions> =
+  T['includeAttachmentInfo'] extends true
+    ? true
+    : T['includeInstalledInfo'] extends true
+      ? true
+      : T['includeUserCountInfo'] extends true
+        ? true
+        : false;
+
+// Final result type based on options
+export type IGrowiAdditionalInfoResult<T extends GrowiInfoOptions> =
+  HasAnyOption<T> extends true ? IGrowiAdditionalInfoByOptions<T> : undefined;
+
+export interface IGrowiInfo<A extends object | undefined = undefined> {
   serviceInstanceId: string;
   serviceInstanceId: string;
   appSiteUrl: string;
   appSiteUrl: string;
   osInfo: IGrowiOSInfo;
   osInfo: IGrowiOSInfo;