Files
gemini-cli/packages/core/src/telemetry/sdk.test.ts

240 lines
8.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Config } from '../config/config.js';
import { initializeTelemetry, shutdownTelemetry } from './sdk.js';
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 { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
GcpTraceExporter,
GcpLogExporter,
GcpMetricExporter,
} from './gcp-exporters.js';
import { TelemetryTarget } from './index.js';
import * as os from 'node:os';
import * as path from 'node:path';
vi.mock('@opentelemetry/exporter-trace-otlp-grpc');
vi.mock('@opentelemetry/exporter-logs-otlp-grpc');
vi.mock('@opentelemetry/exporter-metrics-otlp-grpc');
vi.mock('@opentelemetry/exporter-trace-otlp-http');
vi.mock('@opentelemetry/exporter-logs-otlp-http');
vi.mock('@opentelemetry/exporter-metrics-otlp-http');
vi.mock('@opentelemetry/sdk-node');
vi.mock('./gcp-exporters.js');
describe('Telemetry SDK', () => {
let mockConfig: Config;
beforeEach(() => {
vi.clearAllMocks();
mockConfig = {
getTelemetryEnabled: () => true,
getTelemetryOtlpEndpoint: () => 'http://localhost:4317',
getTelemetryOtlpProtocol: () => 'grpc',
getTelemetryTarget: () => 'local',
getTelemetryUseCollector: () => false,
getTelemetryOutfile: () => undefined,
getDebugMode: () => false,
getSessionId: () => 'test-session',
} as unknown as Config;
});
afterEach(async () => {
await shutdownTelemetry(mockConfig);
});
it('should use gRPC exporters when protocol is grpc', () => {
initializeTelemetry(mockConfig);
expect(OTLPTraceExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(OTLPLogExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(OTLPMetricExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(NodeSDK.prototype.start).toHaveBeenCalled();
});
it('should use HTTP exporters when protocol is http', () => {
vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true);
vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
'http://localhost:4318',
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporterHttp).toHaveBeenCalledWith({
url: 'http://localhost:4318/',
});
expect(OTLPLogExporterHttp).toHaveBeenCalledWith({
url: 'http://localhost:4318/',
});
expect(OTLPMetricExporterHttp).toHaveBeenCalledWith({
url: 'http://localhost:4318/',
});
expect(NodeSDK.prototype.start).toHaveBeenCalled();
});
it('should parse gRPC endpoint correctly', () => {
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
'https://my-collector.com',
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporter).toHaveBeenCalledWith(
expect.objectContaining({ url: 'https://my-collector.com' }),
);
});
it('should parse HTTP endpoint correctly', () => {
vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
'https://my-collector.com',
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporterHttp).toHaveBeenCalledWith(
expect.objectContaining({ url: 'https://my-collector.com/' }),
);
});
it('should use direct GCP exporters when target is gcp, project ID is set, and useCollector is false', () => {
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
TelemetryTarget.GCP,
);
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
const originalEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = 'test-project';
try {
initializeTelemetry(mockConfig);
expect(GcpTraceExporter).toHaveBeenCalledWith('test-project');
expect(GcpLogExporter).toHaveBeenCalledWith('test-project');
expect(GcpMetricExporter).toHaveBeenCalledWith('test-project');
expect(NodeSDK.prototype.start).toHaveBeenCalled();
} finally {
if (originalEnv) {
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalEnv;
} else {
delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
}
}
});
it('should use OTLP exporters when target is gcp but useCollector is true', () => {
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
TelemetryTarget.GCP,
);
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(OTLPLogExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
expect(OTLPMetricExporter).toHaveBeenCalledWith({
url: 'http://localhost:4317',
compression: 'gzip',
});
});
it('should not use GCP exporters when project ID environment variable is not set', () => {
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
TelemetryTarget.GCP,
);
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
const originalOtlpEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
const originalGoogleEnv = process.env['GOOGLE_CLOUD_PROJECT'];
delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
delete process.env['GOOGLE_CLOUD_PROJECT'];
try {
initializeTelemetry(mockConfig);
expect(GcpTraceExporter).not.toHaveBeenCalled();
expect(GcpLogExporter).not.toHaveBeenCalled();
expect(GcpMetricExporter).not.toHaveBeenCalled();
expect(NodeSDK.prototype.start).toHaveBeenCalled();
} finally {
if (originalOtlpEnv) {
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalOtlpEnv;
}
if (originalGoogleEnv) {
process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleEnv;
}
}
});
it('should use GOOGLE_CLOUD_PROJECT as fallback when OTLP_GOOGLE_CLOUD_PROJECT is not set', () => {
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
TelemetryTarget.GCP,
);
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(false);
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
const originalOtlpEnv = process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
const originalGoogleEnv = process.env['GOOGLE_CLOUD_PROJECT'];
delete process.env['OTLP_GOOGLE_CLOUD_PROJECT'];
process.env['GOOGLE_CLOUD_PROJECT'] = 'fallback-project';
try {
initializeTelemetry(mockConfig);
expect(GcpTraceExporter).toHaveBeenCalledWith('fallback-project');
expect(GcpLogExporter).toHaveBeenCalledWith('fallback-project');
expect(GcpMetricExporter).toHaveBeenCalledWith('fallback-project');
expect(NodeSDK.prototype.start).toHaveBeenCalled();
} finally {
if (originalOtlpEnv) {
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = originalOtlpEnv;
}
if (originalGoogleEnv) {
process.env['GOOGLE_CLOUD_PROJECT'] = originalGoogleEnv;
} else {
delete process.env['GOOGLE_CLOUD_PROJECT'];
}
}
});
it('should not use OTLP exporters when telemetryOutfile is set', () => {
vi.spyOn(mockConfig, 'getTelemetryOutfile').mockReturnValue(
path.join(os.tmpdir(), 'test.log'),
);
initializeTelemetry(mockConfig);
expect(OTLPTraceExporter).not.toHaveBeenCalled();
expect(OTLPLogExporter).not.toHaveBeenCalled();
expect(OTLPMetricExporter).not.toHaveBeenCalled();
expect(OTLPTraceExporterHttp).not.toHaveBeenCalled();
expect(OTLPLogExporterHttp).not.toHaveBeenCalled();
expect(OTLPMetricExporterHttp).not.toHaveBeenCalled();
expect(NodeSDK.prototype.start).toHaveBeenCalled();
});
});