Yuki Takei 9 месяцев назад
Родитель
Сommit
10a58aeeb6

+ 15 - 28
apps/app/src/features/opentelemetry/server/anonymization/anonymize-http-requests.ts

@@ -1,38 +1,25 @@
-import { diag } from '@opentelemetry/api';
 import type { InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node';
 
-const logger = diag.createComponentLogger({ namespace: 'growi:anonymization:query' });
-
-const ANONYMIZATION_CONFIG = {
-  sensitiveParams: ['q', 'query', 'search', 'term', 'keyword'],
-  maskPattern: '***',
-};
+import { anonymizationModules } from './handlers';
 
 export const httpInstrumentationConfig: InstrumentationConfigMap['@opentelemetry/instrumentation-http'] = {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  requestHook: (span: any, request: any) => {
-    try {
-      // Anonymize from request URL
-      const url = request.url || '';
-      if (url.includes('?')) {
-        const urlObj = new URL(url, 'http://localhost');
-        let modified = false;
-        ANONYMIZATION_CONFIG.sensitiveParams.forEach((param) => {
-          if (urlObj.searchParams.has(param)) {
-            urlObj.searchParams.set(param, ANONYMIZATION_CONFIG.maskPattern);
-            modified = true;
-          }
-        });
+  startIncomingSpanHook: (request) => {
+    // Get URL from IncomingMessage (server-side requests)
+    const incomingRequest = request;
+    const url = incomingRequest.url || '';
+
+    const attributes = {};
 
-        if (modified) {
-          span.setAttribute('http.url', urlObj.toString());
-          span.setAttribute('http.target', urlObj.pathname + urlObj.search);
-          logger.debug('Anonymized search query parameters');
+    // Use efficient module-based approach
+    for (const anonymizationModule of anonymizationModules) {
+      if (anonymizationModule.canHandle(url)) {
+        const moduleAttributes = anonymizationModule.handle(incomingRequest, url);
+        if (moduleAttributes != null) {
+          Object.assign(attributes, moduleAttributes);
         }
       }
     }
-    catch (error) {
-      logger.error('Failed to anonymize request', { error });
-    }
+
+    return attributes;
   },
 };

+ 12 - 0
apps/app/src/features/opentelemetry/server/anonymization/handlers/index.ts

@@ -0,0 +1,12 @@
+import type { AnonymizationModule } from '../interfaces/anonymization-module';
+
+import { pageListingApiModule } from './page-listing-api-handler';
+import { searchApiModule } from './search-api-handler';
+
+/**
+ * List of anonymization modules
+ */
+export const anonymizationModules: AnonymizationModule[] = [
+  searchApiModule,
+  pageListingApiModule,
+];

+ 46 - 0
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-listing-api-handler.ts

@@ -0,0 +1,46 @@
+import type { IncomingMessage } from 'http';
+
+import { diag } from '@opentelemetry/api';
+
+import { ATTR_HTTP_TARGET } from '../../semconv';
+import type { AnonymizationModule } from '../interfaces/anonymization-module';
+import { anonymizeQueryParams } from '../utils/anonymize-query-params';
+
+const logger = diag.createComponentLogger({ namespace: 'growi:anonymization:page-listing-handler' });
+
+/**
+ * Page listing API anonymization module
+ */
+export const pageListingApiModule: AnonymizationModule = {
+  /**
+   * Check if this module can handle page-listing API endpoints
+   */
+  canHandle(url: string): boolean {
+    return url.includes('/_api/v3/page-listing/ancestors-children')
+      || url.includes('/_api/v3/page-listing/children')
+      || url.includes('/_api/v3/page-listing/info');
+    // Add other page-listing endpoints here as needed
+  },
+
+  /**
+   * Handle anonymization for page-listing API endpoints
+   */
+  handle(request: IncomingMessage, url: string): Record<string, string> | null {
+    const attributes: Record<string, string> = {};
+    let hasAnonymization = false;
+
+    // Handle ancestors-children endpoint
+    if (
+      url.includes('/_api/v3/page-listing/ancestors-children')
+      || url.includes('/_api/v3/page-listing/children')
+      || url.includes('/_api/v3/page-listing/info')
+    ) {
+      const anonymizedUrl = anonymizeQueryParams(url, ['path']);
+      attributes[ATTR_HTTP_TARGET] = anonymizedUrl;
+      hasAnonymization = true;
+      logger.debug(`Anonymized page-listing ancestors-children URL: ${url} -> ${anonymizedUrl}`);
+    }
+
+    return hasAnonymization ? attributes : null;
+  },
+};

+ 39 - 0
apps/app/src/features/opentelemetry/server/anonymization/handlers/search-api-handler.ts

@@ -0,0 +1,39 @@
+import type { IncomingMessage } from 'http';
+
+import { diag } from '@opentelemetry/api';
+
+import { ATTR_HTTP_TARGET } from '../../semconv';
+import type { AnonymizationModule } from '../interfaces/anonymization-module';
+import { anonymizeQueryParams } from '../utils/anonymize-query-params';
+
+const logger = diag.createComponentLogger({ namespace: 'growi:anonymization:search-handler' });
+
+/**
+ * Search API anonymization module
+ */
+export const searchApiModule: AnonymizationModule = {
+  /**
+   * Check if this module can handle search API endpoints
+   */
+  canHandle(url: string): boolean {
+    return url.includes('/_api/search') || url.includes('/_search');
+  },
+
+  /**
+   * Handle anonymization for search API endpoints
+   */
+  handle(request: IncomingMessage, url: string): Record<string, string> | null {
+    // Check if this is a search request that needs anonymization
+    if (url.includes('?q=')) {
+      const anonymizedUrl = anonymizeQueryParams(url, ['q']);
+
+      logger.debug(`Anonymized search API URL: ${url} -> ${anonymizedUrl}`);
+
+      return {
+        [ATTR_HTTP_TARGET]: anonymizedUrl,
+      };
+    }
+
+    return null;
+  },
+};

+ 21 - 0
apps/app/src/features/opentelemetry/server/anonymization/interfaces/anonymization-module.ts

@@ -0,0 +1,21 @@
+import type { IncomingMessage } from 'http';
+
+/**
+ * Interface for anonymization modules
+ */
+export interface AnonymizationModule {
+  /**
+   * Check if this module can handle the given URL
+   * @param url - The request URL
+   * @returns true if this module should process the request
+   */
+  canHandle(url: string): boolean;
+
+  /**
+   * Process anonymization for the request
+   * @param request - The HTTP request
+   * @param url - The request URL
+   * @returns Attributes to be set on the span, or null if no anonymization needed
+   */
+  handle(request: IncomingMessage, url: string): Record<string, string> | null;
+}

+ 42 - 0
apps/app/src/features/opentelemetry/server/anonymization/utils/anonymize-query-params.ts

@@ -0,0 +1,42 @@
+import { diag } from '@opentelemetry/api';
+
+const logger = diag.createComponentLogger({ namespace: 'growi:anonymization:query-params' });
+
+/**
+ * Anonymize specific query parameters in HTTP target URL
+ * @param target - The HTTP target URL with query parameters
+ * @param paramNames - Array of parameter names to anonymize
+ * @returns Anonymized HTTP target URL
+ */
+export function anonymizeQueryParams(target: string, paramNames: string[]): string {
+  try {
+    const url = new URL(target, 'http://localhost');
+    const searchParams = new URLSearchParams(url.search);
+    let hasAnonymization = false;
+
+    // Anonymize each specified parameter if it exists
+    for (const paramName of paramNames) {
+      if (searchParams.has(paramName)) {
+        const originalValue = searchParams.get(paramName);
+        if (originalValue) {
+          // Replace the parameter content with [ANONYMIZED] but keep the parameter structure
+          searchParams.set(paramName, '[ANONYMIZED]');
+          hasAnonymization = true;
+          logger.debug(`Anonymized query parameter '${paramName}': original length=${originalValue.length}`);
+        }
+      }
+    }
+
+    if (!hasAnonymization) {
+      return target; // No changes needed
+    }
+
+    // Reconstruct the target URL with anonymized parameters
+    url.search = searchParams.toString();
+    return url.pathname + url.search;
+  }
+  catch (error) {
+    logger.warn(`Failed to anonymize query parameters [${paramNames.join(', ')}]: ${error}`);
+    return target;
+  }
+}

+ 1 - 0
apps/app/src/features/opentelemetry/server/semconv.ts

@@ -37,3 +37,4 @@ Occasionally, one should review changes to `@opentelemetry/semantic-conventions`
 */
 
 export const ATTR_SERVICE_INSTANCE_ID = 'service.instance.id';
+export const ATTR_HTTP_TARGET = 'http.target';