diff options
Diffstat (limited to 'packages/core/src/telemetry/sdk.ts')
| -rw-r--r-- | packages/core/src/telemetry/sdk.ts | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/packages/core/src/telemetry/sdk.ts b/packages/core/src/telemetry/sdk.ts new file mode 100644 index 00000000..b55cb149 --- /dev/null +++ b/packages/core/src/telemetry/sdk.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { Resource } from '@opentelemetry/resources'; +import { + BatchSpanProcessor, + ConsoleSpanExporter, +} from '@opentelemetry/sdk-trace-node'; +import { + BatchLogRecordProcessor, + ConsoleLogRecordExporter, +} from '@opentelemetry/sdk-logs'; +import { + ConsoleMetricExporter, + PeriodicExportingMetricReader, +} from '@opentelemetry/sdk-metrics'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { Config } from '../config/config.js'; +import { SERVICE_NAME, sessionId } from './constants.js'; +import { initializeMetrics } from './metrics.js'; +import { logCliConfiguration } from './loggers.js'; + +// For troubleshooting, set the log level to DiagLogLevel.DEBUG +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); + +let sdk: NodeSDK | undefined; +let telemetryInitialized = false; + +export function isTelemetrySdkInitialized(): boolean { + return telemetryInitialized; +} + +function parseGrpcEndpoint( + otlpEndpointSetting: string | undefined, +): string | undefined { + if (!otlpEndpointSetting) { + return undefined; + } + // Trim leading/trailing quotes that might come from env variables + const trimmedEndpoint = otlpEndpointSetting.replace(/^["']|["']$/g, ''); + + try { + const url = new URL(trimmedEndpoint); + // OTLP gRPC exporters expect an endpoint in the format scheme://host:port + // The `origin` property provides this, stripping any path, query, or hash. + return url.origin; + } catch (error) { + diag.error('Invalid OTLP endpoint URL provided:', trimmedEndpoint, error); + return undefined; + } +} + +export function initializeTelemetry(config: Config): void { + if (telemetryInitialized || !config.getTelemetryEnabled()) { + return; + } + + const geminiCliVersion = config.getUserAgent() || 'unknown'; + const resource = new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, + [SemanticResourceAttributes.SERVICE_VERSION]: geminiCliVersion, + 'session.id': sessionId, + }); + + const otlpEndpoint = config.getTelemetryOtlpEndpoint(); + const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint); + const useOtlp = !!grpcParsedEndpoint; + + const spanExporter = useOtlp + ? new OTLPTraceExporter({ url: grpcParsedEndpoint }) + : new ConsoleSpanExporter(); + const logExporter = useOtlp + ? new OTLPLogExporter({ url: grpcParsedEndpoint }) + : new ConsoleLogRecordExporter(); + const metricReader = useOtlp + ? new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ url: grpcParsedEndpoint }), + exportIntervalMillis: 10000, + }) + : new PeriodicExportingMetricReader({ + exporter: new ConsoleMetricExporter(), + exportIntervalMillis: 10000, + }); + + sdk = new NodeSDK({ + resource, + spanProcessors: [new BatchSpanProcessor(spanExporter)], + logRecordProcessor: new BatchLogRecordProcessor(logExporter), + metricReader, + instrumentations: [new HttpInstrumentation()], + }); + + try { + sdk.start(); + console.log('OpenTelemetry SDK started successfully.'); + telemetryInitialized = true; + initializeMetrics(); + logCliConfiguration(config); + } catch (error) { + console.error('Error starting OpenTelemetry SDK:', error); + } + + process.on('SIGTERM', shutdownTelemetry); + process.on('SIGINT', shutdownTelemetry); +} + +export async function shutdownTelemetry(): Promise<void> { + if (!telemetryInitialized || !sdk) { + return; + } + try { + await sdk.shutdown(); + console.log('OpenTelemetry SDK shut down successfully.'); + } catch (error) { + console.error('Error shutting down SDK:', error); + } finally { + telemetryInitialized = false; + } +} |
