node-sdk.spec.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import { ConfigSource } from '@growi/core/dist/interfaces';
  2. import { NodeSDK } from '@opentelemetry/sdk-node';
  3. import { configManager } from '~/server/service/config-manager';
  4. import { setupAdditionalResourceAttributes, initInstrumentation, startOpenTelemetry } from './node-sdk';
  5. import { getResource } from './node-sdk-resource';
  6. // Only mock configManager as it's external to what we're testing
  7. vi.mock('~/server/service/config-manager', () => ({
  8. configManager: {
  9. getConfig: vi.fn(),
  10. loadConfigs: vi.fn(),
  11. },
  12. }));
  13. // Mock custom metrics setup
  14. vi.mock('./custom-metrics', () => ({
  15. setupCustomMetrics: vi.fn(),
  16. }));
  17. // Mock growi-info service to avoid database dependencies
  18. vi.mock('~/server/service/growi-info', () => ({
  19. growiInfoService: {
  20. getGrowiInfo: vi.fn().mockResolvedValue({
  21. type: 'app',
  22. deploymentType: 'standalone',
  23. additionalInfo: {
  24. attachmentType: 'local',
  25. installedAt: new Date('2023-01-01T00:00:00.000Z'),
  26. installedAtByOldestUser: new Date('2023-01-01T00:00:00.000Z'),
  27. },
  28. }),
  29. },
  30. }));
  31. describe('node-sdk', () => {
  32. // Helper functions to reduce duplication
  33. const mockInstrumentationEnabled = () => {
  34. vi.mocked(configManager.getConfig).mockImplementation((key: string, source?: ConfigSource) => {
  35. if (key === 'otel:enabled') {
  36. return source === ConfigSource.env ? true : undefined;
  37. }
  38. return undefined;
  39. });
  40. };
  41. const mockInstrumentationDisabled = () => {
  42. vi.mocked(configManager.getConfig).mockImplementation((key: string, source?: ConfigSource) => {
  43. if (key === 'otel:enabled') {
  44. return source === ConfigSource.env ? false : undefined;
  45. }
  46. return undefined;
  47. });
  48. };
  49. beforeEach(async() => {
  50. vi.clearAllMocks();
  51. // Reset SDK instance using __testing__ export
  52. const { __testing__ } = await import('./node-sdk');
  53. __testing__.reset();
  54. // Mock loadConfigs to resolve immediately
  55. vi.mocked(configManager.loadConfigs).mockResolvedValue(undefined);
  56. });
  57. describe('initInstrumentation', () => {
  58. it('should call setupCustomMetrics when instrumentation is enabled', async() => {
  59. // Mock instrumentation as enabled
  60. mockInstrumentationEnabled();
  61. await initInstrumentation();
  62. });
  63. it('should not call setupCustomMetrics when instrumentation is disabled', async() => {
  64. const { setupCustomMetrics } = await import('./custom-metrics');
  65. // Mock instrumentation as disabled
  66. mockInstrumentationDisabled();
  67. await initInstrumentation();
  68. // Verify setupCustomMetrics was not called
  69. expect(setupCustomMetrics).not.toHaveBeenCalled();
  70. });
  71. it('should create SDK instance when instrumentation is enabled', async() => {
  72. // Mock instrumentation as enabled
  73. mockInstrumentationEnabled();
  74. await initInstrumentation();
  75. // Get instance for testing
  76. const { __testing__ } = await import('./node-sdk');
  77. const sdkInstance = __testing__.getSdkInstance();
  78. expect(sdkInstance).toBeDefined();
  79. expect(sdkInstance).toBeInstanceOf(NodeSDK);
  80. });
  81. it('should not create SDK instance when instrumentation is disabled', async() => {
  82. // Mock instrumentation as disabled
  83. mockInstrumentationDisabled();
  84. await initInstrumentation();
  85. // Verify that no SDK instance was created
  86. const { __testing__ } = await import('./node-sdk');
  87. const sdkInstance = __testing__.getSdkInstance();
  88. expect(sdkInstance).toBeUndefined();
  89. });
  90. });
  91. describe('setupAdditionalResourceAttributes', () => {
  92. it('should update service.instance.id when app:serviceInstanceId is available', async() => {
  93. // Set up mocks for this specific test
  94. vi.mocked(configManager.getConfig).mockImplementation((key: string, source?: ConfigSource) => {
  95. // For otel:enabled, always expect ConfigSource.env
  96. if (key === 'otel:enabled') {
  97. return source === ConfigSource.env ? true : undefined;
  98. }
  99. // For service instance IDs, only respond when no source is specified
  100. if (key === 'app:serviceInstanceId') return 'test-instance-id';
  101. return undefined;
  102. });
  103. // Initialize SDK first
  104. await initInstrumentation();
  105. // Get instance for testing
  106. const { __testing__ } = await import('./node-sdk');
  107. const sdkInstance = __testing__.getSdkInstance();
  108. expect(sdkInstance).toBeDefined();
  109. expect(sdkInstance).toBeInstanceOf(NodeSDK);
  110. // Verify initial state (service.instance.id should not be set)
  111. if (sdkInstance == null) {
  112. throw new Error('SDK instance should be defined');
  113. }
  114. const resource = getResource(sdkInstance);
  115. expect(resource).toBeDefined();
  116. expect(resource.attributes['service.instance.id']).toBeUndefined();
  117. // Call setupAdditionalResourceAttributes
  118. await setupAdditionalResourceAttributes();
  119. // Verify that resource was updated with app:serviceInstanceId
  120. const updatedResource = getResource(sdkInstance);
  121. expect(updatedResource.attributes['service.instance.id']).toBe('test-instance-id');
  122. });
  123. it('should update service.instance.id with otel:serviceInstanceId if available', async() => {
  124. // Set up mocks for this specific test
  125. vi.mocked(configManager.getConfig).mockImplementation((key: string, source?: ConfigSource) => {
  126. // For otel:enabled, always expect ConfigSource.env
  127. if (key === 'otel:enabled') {
  128. return source === ConfigSource.env ? true : undefined;
  129. }
  130. // For service instance IDs, only respond when no source is specified
  131. if (source === undefined) {
  132. if (key === 'otel:serviceInstanceId') return 'otel-instance-id';
  133. if (key === 'app:serviceInstanceId') return 'test-instance-id';
  134. }
  135. return undefined;
  136. });
  137. // Initialize SDK
  138. await initInstrumentation();
  139. // Get instance and verify initial state
  140. const { __testing__ } = await import('./node-sdk');
  141. const sdkInstance = __testing__.getSdkInstance();
  142. if (sdkInstance == null) {
  143. throw new Error('SDK instance should be defined');
  144. }
  145. const resource = getResource(sdkInstance);
  146. expect(resource.attributes['service.instance.id']).toBeUndefined();
  147. // Call setupAdditionalResourceAttributes
  148. await setupAdditionalResourceAttributes();
  149. // Verify that otel:serviceInstanceId was used
  150. const updatedResource = getResource(sdkInstance);
  151. expect(updatedResource.attributes['service.instance.id']).toBe('otel-instance-id');
  152. });
  153. it('should handle gracefully when instrumentation is disabled', async() => {
  154. // Mock instrumentation as disabled
  155. mockInstrumentationDisabled();
  156. // Initialize SDK (should not create instance)
  157. await initInstrumentation();
  158. // Call setupAdditionalResourceAttributes should not throw error
  159. await expect(setupAdditionalResourceAttributes()).resolves.toBeUndefined();
  160. });
  161. });
  162. describe('startOpenTelemetry', () => {
  163. it('should start SDK and call setupCustomMetrics when instrumentation is enabled and SDK instance exists', async() => {
  164. const { setupCustomMetrics } = await import('./custom-metrics');
  165. // Mock instrumentation as enabled
  166. mockInstrumentationEnabled();
  167. // Initialize SDK first
  168. await initInstrumentation();
  169. // Get SDK instance and mock its start method
  170. const { __testing__ } = await import('./node-sdk');
  171. const sdkInstance = __testing__.getSdkInstance();
  172. expect(sdkInstance).toBeDefined();
  173. if (sdkInstance != null) {
  174. const startSpy = vi.spyOn(sdkInstance, 'start');
  175. // Call startOpenTelemetry
  176. startOpenTelemetry();
  177. // Verify that start method was called
  178. expect(startSpy).toHaveBeenCalledOnce();
  179. // Verify that setupCustomMetrics was called
  180. expect(setupCustomMetrics).toHaveBeenCalledOnce();
  181. }
  182. });
  183. it('should not start SDK when instrumentation is disabled', async() => {
  184. const { setupCustomMetrics } = await import('./custom-metrics');
  185. // Mock instrumentation as disabled
  186. mockInstrumentationDisabled();
  187. // Initialize SDK (should not create instance)
  188. await initInstrumentation();
  189. // Call startOpenTelemetry
  190. startOpenTelemetry();
  191. // Verify that setupCustomMetrics was not called
  192. expect(setupCustomMetrics).not.toHaveBeenCalled();
  193. });
  194. it('should not start SDK when SDK instance does not exist', async() => {
  195. const { setupCustomMetrics } = await import('./custom-metrics');
  196. // Mock instrumentation as enabled but don't initialize SDK
  197. mockInstrumentationEnabled();
  198. // Call startOpenTelemetry without initializing SDK
  199. startOpenTelemetry();
  200. // Verify that setupCustomMetrics was not called
  201. expect(setupCustomMetrics).not.toHaveBeenCalled();
  202. });
  203. });
  204. });