mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 14:44:29 +00:00
Allow telemetry exporters to GCP to utilize user's login credentials, if requested (#13778)
This commit is contained in:
committed by
GitHub
parent
92e95ed806
commit
b9b3b8050d
@@ -74,15 +74,16 @@ observability framework — Gemini CLI's observability system provides:
|
||||
All telemetry behavior is controlled through your `.gemini/settings.json` file.
|
||||
Environment variables can be used to override the settings in the file.
|
||||
|
||||
| Setting | Environment Variable | Description | Values | Default |
|
||||
| -------------- | -------------------------------- | ------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
| Setting | Environment Variable | Description | Values | Default |
|
||||
| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
|
||||
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
|
||||
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
|
||||
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
|
||||
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
|
||||
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
|
||||
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
|
||||
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
|
||||
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
|
||||
|
||||
**Note on boolean environment variables:** For the boolean settings (`enabled`,
|
||||
`logPrompts`, `useCollector`), setting the corresponding environment variable to
|
||||
@@ -130,6 +131,34 @@ Before using either method below, complete these steps:
|
||||
--project="$OTLP_GOOGLE_CLOUD_PROJECT"
|
||||
```
|
||||
|
||||
### Authenticating with CLI Credentials
|
||||
|
||||
By default, the telemetry collector for Google Cloud uses Application Default
|
||||
Credentials (ADC). However, you can configure it to use the same OAuth
|
||||
credentials that you use to log in to the Gemini CLI. This is useful in
|
||||
environments where you don't have ADC set up.
|
||||
|
||||
To enable this, set the `useCliAuth` property in your `telemetry` settings to
|
||||
`true`:
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp",
|
||||
"useCliAuth": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:**
|
||||
|
||||
- This setting requires the use of **Direct Export** (in-process exporters).
|
||||
- It **cannot** be used with `useCollector: true`. If you enable both, telemetry
|
||||
will be disabled and an error will be logged.
|
||||
- The CLI will automatically use your credentials to authenticate with Google
|
||||
Cloud Trace, Metrics, and Logging APIs.
|
||||
|
||||
### Direct export (recommended)
|
||||
|
||||
Sends telemetry directly to Google Cloud services. No collector needed.
|
||||
|
||||
@@ -13,6 +13,15 @@ import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
isEditorAvailable: () => true, // Mock to behave predictably in CI
|
||||
};
|
||||
});
|
||||
|
||||
// Mock editorSettingsManager
|
||||
vi.mock('../editors/editorSettingsManager.js', () => ({
|
||||
editorSettingsManager: {
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`EditorSettingsDialog > renders correctly 1`] = `
|
||||
│ 2. Vim These editors are currently supported. Please note │
|
||||
│ that some editors cannot be used in sandbox mode. │
|
||||
│ Apply To │
|
||||
│ ● 1. User Settings Your preferred editor is: None. │
|
||||
│ ● 1. User Settings Your preferred editor is: VS Code. │
|
||||
│ 2. Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change │
|
||||
|
||||
@@ -16,6 +16,10 @@ const mocks = vi.hoisted(() => ({
|
||||
copyFile: vi.fn(),
|
||||
homedir: vi.fn(),
|
||||
platform: vi.fn(),
|
||||
writeStream: {
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
@@ -24,6 +28,7 @@ vi.mock('node:child_process', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
createWriteStream: () => mocks.writeStream,
|
||||
promises: {
|
||||
mkdir: mocks.mkdir,
|
||||
readFile: mocks.readFile,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
resetOauthClientForTesting,
|
||||
clearCachedCredentialFile,
|
||||
clearOauthClientCache,
|
||||
authEvents,
|
||||
} from './oauth2.js';
|
||||
import { UserAccountManager } from '../utils/userAccountManager.js';
|
||||
import { OAuth2Client, Compute, GoogleAuth } from 'google-auth-library';
|
||||
@@ -109,13 +110,18 @@ describe('oauth2', () => {
|
||||
const mockGetAccessToken = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ token: 'mock-access-token' });
|
||||
let tokensListener: ((tokens: Credentials) => void) | undefined;
|
||||
const mockOAuth2Client = {
|
||||
generateAuthUrl: mockGenerateAuthUrl,
|
||||
getToken: mockGetToken,
|
||||
setCredentials: mockSetCredentials,
|
||||
getAccessToken: mockGetAccessToken,
|
||||
credentials: mockTokens,
|
||||
on: vi.fn(),
|
||||
on: vi.fn((event, listener) => {
|
||||
if (event === 'tokens') {
|
||||
tokensListener = listener;
|
||||
}
|
||||
}),
|
||||
} as unknown as OAuth2Client;
|
||||
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
|
||||
|
||||
@@ -195,6 +201,11 @@ describe('oauth2', () => {
|
||||
});
|
||||
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
|
||||
|
||||
// Manually trigger the 'tokens' event listener
|
||||
if (tokensListener) {
|
||||
await tokensListener(mockTokens);
|
||||
}
|
||||
|
||||
// Verify Google Account was cached
|
||||
const googleAccountPath = path.join(
|
||||
tempHomeDir,
|
||||
@@ -215,6 +226,45 @@ describe('oauth2', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear credentials file', async () => {
|
||||
// Setup initial state with files
|
||||
const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
|
||||
|
||||
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
|
||||
await fs.promises.writeFile(credsPath, '{}');
|
||||
|
||||
await clearCachedCredentialFile();
|
||||
|
||||
expect(fs.existsSync(credsPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('should emit post_auth event when loading cached credentials', async () => {
|
||||
const cachedCreds = { refresh_token: 'cached-token' };
|
||||
const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
|
||||
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
|
||||
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
|
||||
|
||||
const mockClient = {
|
||||
setCredentials: vi.fn(),
|
||||
getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
|
||||
getTokenInfo: vi.fn().mockResolvedValue({}),
|
||||
on: vi.fn(),
|
||||
};
|
||||
vi.mocked(OAuth2Client).mockImplementation(
|
||||
() => mockClient as unknown as OAuth2Client,
|
||||
);
|
||||
|
||||
const eventPromise = new Promise<void>((resolve) => {
|
||||
authEvents.once('post_auth', (creds) => {
|
||||
expect(creds.refresh_token).toBe('cached-token');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, mockConfig);
|
||||
await eventPromise;
|
||||
});
|
||||
|
||||
it('should perform login with user code', async () => {
|
||||
const mockConfigWithNoBrowser = {
|
||||
getNoBrowser: () => true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as http from 'node:http';
|
||||
import url from 'node:url';
|
||||
import crypto from 'node:crypto';
|
||||
import * as net from 'node:net';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import open from 'open';
|
||||
import path from 'node:path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
@@ -45,6 +46,22 @@ import {
|
||||
} from '../utils/terminal.js';
|
||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||
|
||||
export const authEvents = new EventEmitter();
|
||||
|
||||
async function triggerPostAuthCallbacks(tokens: Credentials) {
|
||||
// Construct a JWTInput object to pass to callbacks, as this is the
|
||||
// type expected by the downstream Google Cloud client libraries.
|
||||
const jwtInput: JWTInput = {
|
||||
client_id: OAUTH_CLIENT_ID,
|
||||
client_secret: OAUTH_CLIENT_SECRET,
|
||||
refresh_token: tokens.refresh_token ?? undefined, // Ensure null is not passed
|
||||
type: 'authorized_user',
|
||||
};
|
||||
|
||||
// Execute all registered post-authentication callbacks.
|
||||
authEvents.emit('post_auth', jwtInput);
|
||||
}
|
||||
|
||||
const userAccountManager = new UserAccountManager();
|
||||
|
||||
// OAuth Client ID used to initiate OAuth2Client class.
|
||||
@@ -139,6 +156,8 @@ async function initOauthClient(
|
||||
} else {
|
||||
await cacheCredentials(tokens);
|
||||
}
|
||||
|
||||
await triggerPostAuthCallbacks(tokens);
|
||||
});
|
||||
|
||||
if (credentials) {
|
||||
@@ -162,6 +181,8 @@ async function initOauthClient(
|
||||
}
|
||||
}
|
||||
debugLogger.log('Loaded cached credentials.');
|
||||
await triggerPostAuthCallbacks(credentials as Credentials);
|
||||
|
||||
return client;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -570,19 +591,6 @@ async function fetchCachedCredentials(): Promise<
|
||||
return null;
|
||||
}
|
||||
|
||||
async function cacheCredentials(credentials: Credentials) {
|
||||
const filePath = Storage.getOAuthCredsPath();
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
const credString = JSON.stringify(credentials, null, 2);
|
||||
await fs.writeFile(filePath, credString, { mode: 0o600 });
|
||||
try {
|
||||
await fs.chmod(filePath, 0o600);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearOauthClientCache() {
|
||||
oauthClientPromises.clear();
|
||||
}
|
||||
@@ -640,3 +648,16 @@ async function fetchAndCacheUserInfo(client: OAuth2Client): Promise<void> {
|
||||
export function resetOauthClientForTesting() {
|
||||
oauthClientPromises.clear();
|
||||
}
|
||||
|
||||
async function cacheCredentials(credentials: Credentials) {
|
||||
const filePath = Storage.getOAuthCredsPath();
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
const credString = JSON.stringify(credentials, null, 2);
|
||||
await fs.writeFile(filePath, credString, { mode: 0o600 });
|
||||
try {
|
||||
await fs.chmod(filePath, 0o600);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export interface TelemetrySettings {
|
||||
logPrompts?: boolean;
|
||||
outfile?: string;
|
||||
useCollector?: boolean;
|
||||
useCliAuth?: boolean;
|
||||
}
|
||||
|
||||
export interface OutputSettings {
|
||||
@@ -475,6 +476,7 @@ export class Config {
|
||||
logPrompts: params.telemetry?.logPrompts ?? true,
|
||||
outfile: params.telemetry?.outfile,
|
||||
useCollector: params.telemetry?.useCollector,
|
||||
useCliAuth: params.telemetry?.useCliAuth,
|
||||
};
|
||||
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;
|
||||
|
||||
@@ -1067,6 +1069,10 @@ export class Config {
|
||||
return this.telemetrySettings.useCollector ?? false;
|
||||
}
|
||||
|
||||
getTelemetryUseCliAuth(): boolean {
|
||||
return this.telemetrySettings.useCliAuth ?? false;
|
||||
}
|
||||
|
||||
getGeminiClient(): GeminiClient {
|
||||
return this.geminiClient;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ vi.mock('node:fs', () => {
|
||||
});
|
||||
}),
|
||||
existsSync: vi.fn((path: string) => mockFileSystem.has(path)),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -48,6 +48,10 @@ vi.mock('node:fs', () => {
|
||||
});
|
||||
}),
|
||||
existsSync: vi.fn((path: string) => mockFileSystem.has(path)),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -120,9 +120,37 @@ describe('telemetry/config helpers', () => {
|
||||
logPrompts: false,
|
||||
outfile: 'argv.log',
|
||||
useCollector: true, // from env as no argv option
|
||||
useCliAuth: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves useCliAuth from settings', async () => {
|
||||
const settings = {
|
||||
useCliAuth: true,
|
||||
};
|
||||
const resolved = await resolveTelemetrySettings({ settings });
|
||||
expect(resolved.useCliAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('resolves useCliAuth from env', async () => {
|
||||
const env = {
|
||||
GEMINI_TELEMETRY_USE_CLI_AUTH: 'true',
|
||||
};
|
||||
const resolved = await resolveTelemetrySettings({ env });
|
||||
expect(resolved.useCliAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('env overrides settings for useCliAuth', async () => {
|
||||
const settings = {
|
||||
useCliAuth: false,
|
||||
};
|
||||
const env = {
|
||||
GEMINI_TELEMETRY_USE_CLI_AUTH: 'true',
|
||||
};
|
||||
const resolved = await resolveTelemetrySettings({ env, settings });
|
||||
expect(resolved.useCliAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('falls back to OTEL_EXPORTER_OTLP_ENDPOINT when GEMINI var is missing', async () => {
|
||||
const settings = {};
|
||||
const env = {
|
||||
|
||||
@@ -116,5 +116,8 @@ export async function resolveTelemetrySettings(options: {
|
||||
logPrompts,
|
||||
outfile,
|
||||
useCollector,
|
||||
useCliAuth:
|
||||
parseBooleanEnvFlag(env['GEMINI_TELEMETRY_USE_CLI_AUTH']) ??
|
||||
settings.useCliAuth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type JWTInput } from 'google-auth-library';
|
||||
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
|
||||
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
|
||||
import { Logging } from '@google-cloud/logging';
|
||||
@@ -20,9 +21,10 @@ import type {
|
||||
* Google Cloud Trace exporter that extends the official trace exporter
|
||||
*/
|
||||
export class GcpTraceExporter extends TraceExporter {
|
||||
constructor(projectId?: string) {
|
||||
constructor(projectId?: string, credentials?: JWTInput) {
|
||||
super({
|
||||
projectId,
|
||||
credentials,
|
||||
resourceFilter: /^gcp\./,
|
||||
});
|
||||
}
|
||||
@@ -32,9 +34,10 @@ export class GcpTraceExporter extends TraceExporter {
|
||||
* Google Cloud Monitoring exporter that extends the official metrics exporter
|
||||
*/
|
||||
export class GcpMetricExporter extends MetricExporter {
|
||||
constructor(projectId?: string) {
|
||||
constructor(projectId?: string, credentials?: JWTInput) {
|
||||
super({
|
||||
projectId,
|
||||
credentials,
|
||||
prefix: 'custom.googleapis.com/gemini_cli',
|
||||
});
|
||||
}
|
||||
@@ -48,8 +51,8 @@ export class GcpLogExporter implements LogRecordExporter {
|
||||
private log: Log;
|
||||
private pendingWrites: Array<Promise<void>> = [];
|
||||
|
||||
constructor(projectId?: string) {
|
||||
this.logging = new Logging({ projectId });
|
||||
constructor(projectId?: string, credentials?: JWTInput) {
|
||||
this.logging = new Logging({ projectId, credentials });
|
||||
this.log = this.logging.log('gemini_cli');
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ describe('loggers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(true);
|
||||
vi.spyOn(sdk, 'bufferTelemetryEvent').mockImplementation((cb) => cb());
|
||||
vi.spyOn(logs, 'getLogger').mockReturnValue(mockLogger);
|
||||
vi.spyOn(uiTelemetry.uiTelemetryService, 'addEvent').mockImplementation(
|
||||
mockUiEvent.addEvent,
|
||||
@@ -1719,6 +1720,7 @@ describe('loggers', () => {
|
||||
|
||||
it('should only log to Clearcut if OTEL SDK is not initialized', () => {
|
||||
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(false);
|
||||
vi.spyOn(sdk, 'bufferTelemetryEvent').mockImplementation(() => {});
|
||||
const event = new ModelRoutingEvent(
|
||||
'gemini-pro',
|
||||
'default',
|
||||
@@ -2086,4 +2088,19 @@ describe('loggers', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Telemetry Buffering', () => {
|
||||
it('should buffer events when SDK is not initialized', () => {
|
||||
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(false);
|
||||
const bufferSpy = vi
|
||||
.spyOn(sdk, 'bufferTelemetryEvent')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const mockConfig = makeFakeConfig();
|
||||
const event = new StartSessionEvent(mockConfig);
|
||||
logCliConfiguration(mockConfig, event);
|
||||
|
||||
expect(bufferSpy).toHaveBeenCalled();
|
||||
expect(mockLogger.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
recordLinesChanged,
|
||||
recordHookCallMetrics,
|
||||
} from './metrics.js';
|
||||
import { isTelemetrySdkInitialized } from './sdk.js';
|
||||
import { bufferTelemetryEvent } from './sdk.js';
|
||||
import type { UiEvent } from './uiTelemetry.js';
|
||||
import { uiTelemetryService } from './uiTelemetry.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
@@ -79,27 +79,27 @@ export function logCliConfiguration(
|
||||
event: StartSessionEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logStartSessionEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logUserPrompt(config: Config, event: UserPromptEvent): void {
|
||||
ClearcutLogger.getInstance(config)?.logNewPromptEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
@@ -110,35 +110,35 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
ClearcutLogger.getInstance(config)?.logToolCallEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordToolCallMetrics(config, event.duration_ms, {
|
||||
function_name: event.function_name,
|
||||
success: event.success,
|
||||
decision: event.decision,
|
||||
tool_type: event.tool_type,
|
||||
});
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordToolCallMetrics(config, event.duration_ms, {
|
||||
function_name: event.function_name,
|
||||
success: event.success,
|
||||
decision: event.decision,
|
||||
tool_type: event.tool_type,
|
||||
if (event.metadata) {
|
||||
const added = event.metadata['model_added_lines'];
|
||||
if (typeof added === 'number' && added > 0) {
|
||||
recordLinesChanged(config, added, 'added', {
|
||||
function_name: event.function_name,
|
||||
});
|
||||
}
|
||||
const removed = event.metadata['model_removed_lines'];
|
||||
if (typeof removed === 'number' && removed > 0) {
|
||||
recordLinesChanged(config, removed, 'removed', {
|
||||
function_name: event.function_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (event.metadata) {
|
||||
const added = event.metadata['model_added_lines'];
|
||||
if (typeof added === 'number' && added > 0) {
|
||||
recordLinesChanged(config, added, 'added', {
|
||||
function_name: event.function_name,
|
||||
});
|
||||
}
|
||||
const removed = event.metadata['model_removed_lines'];
|
||||
if (typeof removed === 'number' && removed > 0) {
|
||||
recordLinesChanged(config, removed, 'removed', {
|
||||
function_name: event.function_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logToolOutputTruncated(
|
||||
@@ -146,14 +146,14 @@ export function logToolOutputTruncated(
|
||||
event: ToolOutputTruncatedEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logToolOutputTruncatedEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logFileOperation(
|
||||
@@ -161,31 +161,31 @@ export function logFileOperation(
|
||||
event: FileOperationEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logFileOperationEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
recordFileOperationMetric(config, {
|
||||
operation: event.operation,
|
||||
lines: event.lines,
|
||||
mimetype: event.mimetype,
|
||||
extension: event.extension,
|
||||
programming_language: event.programming_language,
|
||||
recordFileOperationMetric(config, {
|
||||
operation: event.operation,
|
||||
lines: event.lines,
|
||||
mimetype: event.mimetype,
|
||||
extension: event.extension,
|
||||
programming_language: event.programming_language,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function logApiRequest(config: Config, event: ApiRequestEvent): void {
|
||||
ClearcutLogger.getInstance(config)?.logApiRequestEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
});
|
||||
}
|
||||
|
||||
export function logFlashFallback(
|
||||
@@ -193,14 +193,14 @@ export function logFlashFallback(
|
||||
event: FlashFallbackEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logFlashFallbackEvent();
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logRipgrepFallback(
|
||||
@@ -208,14 +208,14 @@ export function logRipgrepFallback(
|
||||
event: RipgrepFallbackEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logRipgrepFallbackEvent();
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logApiError(config: Config, event: ApiErrorEvent): void {
|
||||
@@ -226,26 +226,26 @@ export function logApiError(config: Config, event: ApiErrorEvent): void {
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
ClearcutLogger.getInstance(config)?.logApiErrorEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
recordApiErrorMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
status_code: event.status_code,
|
||||
error_type: event.error_type,
|
||||
});
|
||||
|
||||
recordApiErrorMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
status_code: event.status_code,
|
||||
error_type: event.error_type,
|
||||
});
|
||||
|
||||
// Record GenAI operation duration for errors
|
||||
recordApiResponseMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
status_code: event.status_code,
|
||||
genAiAttributes: {
|
||||
...getConventionAttributes(event),
|
||||
'error.type': event.error_type || 'unknown',
|
||||
},
|
||||
// Record GenAI operation duration for errors
|
||||
recordApiResponseMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
status_code: event.status_code,
|
||||
genAiAttributes: {
|
||||
...getConventionAttributes(event),
|
||||
'error.type': event.error_type || 'unknown',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,35 +257,35 @@ export function logApiResponse(config: Config, event: ApiResponseEvent): void {
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
ClearcutLogger.getInstance(config)?.logApiResponseEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
logger.emit(event.toLogRecord(config));
|
||||
logger.emit(event.toSemanticLogRecord(config));
|
||||
const conventionAttributes = getConventionAttributes(event);
|
||||
|
||||
const conventionAttributes = getConventionAttributes(event);
|
||||
|
||||
recordApiResponseMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
status_code: event.status_code,
|
||||
genAiAttributes: conventionAttributes,
|
||||
});
|
||||
|
||||
const tokenUsageData = [
|
||||
{ count: event.usage.input_token_count, type: 'input' as const },
|
||||
{ count: event.usage.output_token_count, type: 'output' as const },
|
||||
{ count: event.usage.cached_content_token_count, type: 'cache' as const },
|
||||
{ count: event.usage.thoughts_token_count, type: 'thought' as const },
|
||||
{ count: event.usage.tool_token_count, type: 'tool' as const },
|
||||
];
|
||||
|
||||
for (const { count, type } of tokenUsageData) {
|
||||
recordTokenUsageMetrics(config, count, {
|
||||
recordApiResponseMetrics(config, event.duration_ms, {
|
||||
model: event.model,
|
||||
type,
|
||||
status_code: event.status_code,
|
||||
genAiAttributes: conventionAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
const tokenUsageData = [
|
||||
{ count: event.usage.input_token_count, type: 'input' as const },
|
||||
{ count: event.usage.output_token_count, type: 'output' as const },
|
||||
{ count: event.usage.cached_content_token_count, type: 'cache' as const },
|
||||
{ count: event.usage.thoughts_token_count, type: 'thought' as const },
|
||||
{ count: event.usage.tool_token_count, type: 'tool' as const },
|
||||
];
|
||||
|
||||
for (const { count, type } of tokenUsageData) {
|
||||
recordTokenUsageMetrics(config, count, {
|
||||
model: event.model,
|
||||
type,
|
||||
genAiAttributes: conventionAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function logLoopDetected(
|
||||
@@ -293,14 +293,14 @@ export function logLoopDetected(
|
||||
event: LoopDetectedEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logLoopDetectedEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logLoopDetectionDisabled(
|
||||
@@ -308,14 +308,14 @@ export function logLoopDetectionDisabled(
|
||||
event: LoopDetectionDisabledEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logLoopDetectionDisabledEvent();
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logNextSpeakerCheck(
|
||||
@@ -323,14 +323,14 @@ export function logNextSpeakerCheck(
|
||||
event: NextSpeakerCheckEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logNextSpeakerCheck(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logSlashCommand(
|
||||
@@ -338,14 +338,14 @@ export function logSlashCommand(
|
||||
event: SlashCommandEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logSlashCommandEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logIdeConnection(
|
||||
@@ -353,14 +353,14 @@ export function logIdeConnection(
|
||||
event: IdeConnectionEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logIdeConnectionEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logConversationFinishedEvent(
|
||||
@@ -368,14 +368,14 @@ export function logConversationFinishedEvent(
|
||||
event: ConversationFinishedEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logConversationFinishedEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logChatCompression(
|
||||
@@ -402,14 +402,14 @@ export function logMalformedJsonResponse(
|
||||
event: MalformedJsonResponseEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logMalformedJsonResponseEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logInvalidChunk(
|
||||
@@ -417,15 +417,15 @@ export function logInvalidChunk(
|
||||
event: InvalidChunkEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logInvalidChunkEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordInvalidChunk(config);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordInvalidChunk(config);
|
||||
});
|
||||
}
|
||||
|
||||
export function logContentRetry(
|
||||
@@ -433,15 +433,15 @@ export function logContentRetry(
|
||||
event: ContentRetryEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logContentRetryEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordContentRetry(config);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordContentRetry(config);
|
||||
});
|
||||
}
|
||||
|
||||
export function logContentRetryFailure(
|
||||
@@ -449,15 +449,15 @@ export function logContentRetryFailure(
|
||||
event: ContentRetryFailureEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logContentRetryFailureEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordContentRetryFailure(config);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordContentRetryFailure(config);
|
||||
});
|
||||
}
|
||||
|
||||
export function logModelRouting(
|
||||
@@ -465,15 +465,15 @@ export function logModelRouting(
|
||||
event: ModelRoutingEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logModelRoutingEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordModelRoutingMetrics(config, event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordModelRoutingMetrics(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
export function logModelSlashCommand(
|
||||
@@ -481,15 +481,15 @@ export function logModelSlashCommand(
|
||||
event: ModelSlashCommandEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logModelSlashCommandEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordModelSlashCommand(config, event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordModelSlashCommand(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
export async function logExtensionInstallEvent(
|
||||
@@ -497,14 +497,14 @@ export async function logExtensionInstallEvent(
|
||||
event: ExtensionInstallEvent,
|
||||
): Promise<void> {
|
||||
await ClearcutLogger.getInstance(config)?.logExtensionInstallEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export async function logExtensionUninstall(
|
||||
@@ -512,14 +512,14 @@ export async function logExtensionUninstall(
|
||||
event: ExtensionUninstallEvent,
|
||||
): Promise<void> {
|
||||
await ClearcutLogger.getInstance(config)?.logExtensionUninstallEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export async function logExtensionUpdateEvent(
|
||||
@@ -527,14 +527,14 @@ export async function logExtensionUpdateEvent(
|
||||
event: ExtensionUpdateEvent,
|
||||
): Promise<void> {
|
||||
await ClearcutLogger.getInstance(config)?.logExtensionUpdateEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export async function logExtensionEnable(
|
||||
@@ -542,14 +542,14 @@ export async function logExtensionEnable(
|
||||
event: ExtensionEnableEvent,
|
||||
): Promise<void> {
|
||||
await ClearcutLogger.getInstance(config)?.logExtensionEnableEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export async function logExtensionDisable(
|
||||
@@ -557,14 +557,14 @@ export async function logExtensionDisable(
|
||||
event: ExtensionDisableEvent,
|
||||
): Promise<void> {
|
||||
await ClearcutLogger.getInstance(config)?.logExtensionDisableEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logSmartEditStrategy(
|
||||
@@ -572,14 +572,14 @@ export function logSmartEditStrategy(
|
||||
event: SmartEditStrategyEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logSmartEditStrategyEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logSmartEditCorrectionEvent(
|
||||
@@ -587,40 +587,40 @@ export function logSmartEditCorrectionEvent(
|
||||
event: SmartEditCorrectionEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logSmartEditCorrectionEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logAgentStart(config: Config, event: AgentStartEvent): void {
|
||||
ClearcutLogger.getInstance(config)?.logAgentStartEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logAgentFinish(config: Config, event: AgentFinishEvent): void {
|
||||
ClearcutLogger.getInstance(config)?.logAgentFinishEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
recordAgentRunMetrics(config, event);
|
||||
recordAgentRunMetrics(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
export function logRecoveryAttempt(
|
||||
@@ -628,16 +628,16 @@ export function logRecoveryAttempt(
|
||||
event: RecoveryAttemptEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logRecoveryAttemptEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
recordRecoveryAttemptMetrics(config, event);
|
||||
recordRecoveryAttemptMetrics(config, event);
|
||||
});
|
||||
}
|
||||
|
||||
export function logWebFetchFallbackAttempt(
|
||||
@@ -645,14 +645,14 @@ export function logWebFetchFallbackAttempt(
|
||||
event: WebFetchFallbackAttemptEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logWebFetchFallbackAttemptEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logLlmLoopCheck(
|
||||
@@ -660,31 +660,31 @@ export function logLlmLoopCheck(
|
||||
event: LlmLoopCheckEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logLlmLoopCheckEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
}
|
||||
|
||||
export function logHookCall(config: Config, event: HookCallEvent): void {
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
|
||||
recordHookCallMetrics(
|
||||
config,
|
||||
event.hook_event_name,
|
||||
event.hook_name,
|
||||
event.duration_ms,
|
||||
event.success,
|
||||
);
|
||||
recordHookCallMetrics(
|
||||
config,
|
||||
event.hook_event_name,
|
||||
event.hook_name,
|
||||
event.duration_ms,
|
||||
event.success,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { initializeTelemetry, shutdownTelemetry } from './sdk.js';
|
||||
import {
|
||||
initializeTelemetry,
|
||||
shutdownTelemetry,
|
||||
bufferTelemetryEvent,
|
||||
} 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 { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
import {
|
||||
GcpTraceExporter,
|
||||
GcpLogExporter,
|
||||
@@ -24,20 +30,40 @@ import { TelemetryTarget } from './index.js';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { authEvents } from '../code_assist/oauth2.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
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-trace-node');
|
||||
vi.mock('@opentelemetry/sdk-node');
|
||||
vi.mock('./gcp-exporters.js');
|
||||
vi.mock('google-auth-library');
|
||||
vi.mock('../utils/debugLogger.js', () => ({
|
||||
debugLogger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Telemetry SDK', () => {
|
||||
let mockConfig: Config;
|
||||
const mockGetApplicationDefault = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(GoogleAuth).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getApplicationDefault: mockGetApplicationDefault,
|
||||
}) as unknown as GoogleAuth,
|
||||
);
|
||||
mockConfig = {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryOtlpEndpoint: () => 'http://localhost:4317',
|
||||
@@ -47,6 +73,8 @@ describe('Telemetry SDK', () => {
|
||||
getTelemetryOutfile: () => undefined,
|
||||
getDebugMode: () => false,
|
||||
getSessionId: () => 'test-session',
|
||||
getTelemetryUseCliAuth: () => false,
|
||||
isInteractive: () => false,
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
@@ -54,8 +82,8 @@ describe('Telemetry SDK', () => {
|
||||
await shutdownTelemetry(mockConfig);
|
||||
});
|
||||
|
||||
it('should use gRPC exporters when protocol is grpc', () => {
|
||||
initializeTelemetry(mockConfig);
|
||||
it('should use gRPC exporters when protocol is grpc', async () => {
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(OTLPTraceExporter).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:4317',
|
||||
@@ -72,14 +100,14 @@ describe('Telemetry SDK', () => {
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use HTTP exporters when protocol is http', () => {
|
||||
it('should use HTTP exporters when protocol is http', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryEnabled').mockReturnValue(true);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
|
||||
'http://localhost:4318',
|
||||
);
|
||||
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(OTLPTraceExporterHttp).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:4318/',
|
||||
@@ -93,28 +121,29 @@ describe('Telemetry SDK', () => {
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should parse gRPC endpoint correctly', () => {
|
||||
it('should parse gRPC endpoint correctly', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
|
||||
'https://my-collector.com',
|
||||
);
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
expect(OTLPTraceExporter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ url: 'https://my-collector.com' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse HTTP endpoint correctly', () => {
|
||||
it('should parse HTTP endpoint correctly', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpProtocol').mockReturnValue('http');
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue(
|
||||
'https://my-collector.com',
|
||||
);
|
||||
initializeTelemetry(mockConfig);
|
||||
await 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', () => {
|
||||
it('should use direct GCP exporters when target is gcp, project ID is set, and useCollector is false', async () => {
|
||||
mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
@@ -125,11 +154,11 @@ describe('Telemetry SDK', () => {
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] = 'test-project';
|
||||
|
||||
try {
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith('test-project');
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith('test-project');
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith('test-project');
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith('test-project', undefined);
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith('test-project', undefined);
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith('test-project', undefined);
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalEnv) {
|
||||
@@ -140,13 +169,13 @@ describe('Telemetry SDK', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should use OTLP exporters when target is gcp but useCollector is true', () => {
|
||||
it('should use OTLP exporters when target is gcp but useCollector is true', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);
|
||||
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(OTLPTraceExporter).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:4317',
|
||||
@@ -162,7 +191,8 @@ describe('Telemetry SDK', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use GCP exporters when project ID environment variable is not set', () => {
|
||||
it('should use GCP exporters even when project ID environment variable is not set', async () => {
|
||||
mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
@@ -175,11 +205,11 @@ describe('Telemetry SDK', () => {
|
||||
delete process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
|
||||
try {
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(GcpTraceExporter).not.toHaveBeenCalled();
|
||||
expect(GcpLogExporter).not.toHaveBeenCalled();
|
||||
expect(GcpMetricExporter).not.toHaveBeenCalled();
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalOtlpEnv) {
|
||||
@@ -191,7 +221,8 @@ describe('Telemetry SDK', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should use GOOGLE_CLOUD_PROJECT as fallback when OTLP_GOOGLE_CLOUD_PROJECT is not set', () => {
|
||||
it('should use GOOGLE_CLOUD_PROJECT as fallback when OTLP_GOOGLE_CLOUD_PROJECT is not set', async () => {
|
||||
mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
@@ -204,11 +235,20 @@ describe('Telemetry SDK', () => {
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = 'fallback-project';
|
||||
|
||||
try {
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith('fallback-project');
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith('fallback-project');
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith('fallback-project');
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith(
|
||||
'fallback-project',
|
||||
undefined,
|
||||
);
|
||||
expect(GcpLogExporter).toHaveBeenCalledWith(
|
||||
'fallback-project',
|
||||
undefined,
|
||||
);
|
||||
expect(GcpMetricExporter).toHaveBeenCalledWith(
|
||||
'fallback-project',
|
||||
undefined,
|
||||
);
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalOtlpEnv) {
|
||||
@@ -222,11 +262,11 @@ describe('Telemetry SDK', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should not use OTLP exporters when telemetryOutfile is set', () => {
|
||||
it('should not use OTLP exporters when telemetryOutfile is set', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryOutfile').mockReturnValue(
|
||||
path.join(os.tmpdir(), 'test.log'),
|
||||
);
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(OTLPTraceExporter).not.toHaveBeenCalled();
|
||||
expect(OTLPLogExporter).not.toHaveBeenCalled();
|
||||
@@ -236,4 +276,98 @@ describe('Telemetry SDK', () => {
|
||||
expect(OTLPMetricExporterHttp).not.toHaveBeenCalled();
|
||||
expect(NodeSDK.prototype.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should defer initialization when useCliAuth is true and no credentials are provided', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
|
||||
|
||||
// 1. Initial state: No credentials.
|
||||
// Should NOT initialize any exporters.
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
// Verify nothing was initialized
|
||||
expect(ConsoleSpanExporter).not.toHaveBeenCalled();
|
||||
expect(GcpTraceExporter).not.toHaveBeenCalled();
|
||||
|
||||
// Verify deferral log
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('deferring telemetry initialization'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize with GCP exporters when credentials are provided via post_auth', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);
|
||||
vi.spyOn(mockConfig, 'getTelemetryTarget').mockReturnValue(
|
||||
TelemetryTarget.GCP,
|
||||
);
|
||||
vi.spyOn(mockConfig, 'getTelemetryOtlpEndpoint').mockReturnValue('');
|
||||
|
||||
// 1. Initial state: No credentials.
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
// Verify nothing happened yet
|
||||
expect(GcpTraceExporter).not.toHaveBeenCalled();
|
||||
|
||||
// 2. Set project ID and emit post_auth event
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
|
||||
|
||||
const mockCredentials = {
|
||||
client_email: 'test@example.com',
|
||||
private_key: '-----BEGIN PRIVATE KEY-----\n...',
|
||||
type: 'authorized_user',
|
||||
};
|
||||
|
||||
// Emit the event directly
|
||||
authEvents.emit('post_auth', mockCredentials);
|
||||
|
||||
// Wait for the event handler to process.
|
||||
await vi.waitFor(() => {
|
||||
// Check if debugLogger was called, which indicates the listener ran
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
'Telemetry reinit with credentials: ',
|
||||
mockCredentials,
|
||||
);
|
||||
|
||||
// Should use GCP exporters now with the project ID
|
||||
expect(GcpTraceExporter).toHaveBeenCalledWith(
|
||||
'test-project',
|
||||
mockCredentials,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bufferTelemetryEvent', () => {
|
||||
it('should execute immediately if SDK is initialized', async () => {
|
||||
await initializeTelemetry(mockConfig);
|
||||
const callback = vi.fn();
|
||||
bufferTelemetryEvent(callback);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should buffer if SDK is not initialized, and flush on initialization', async () => {
|
||||
const callback = vi.fn();
|
||||
bufferTelemetryEvent(callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await initializeTelemetry(mockConfig);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable telemetry and log error if useCollector and useCliAuth are both true', async () => {
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCollector').mockReturnValue(true);
|
||||
vi.spyOn(mockConfig, 'getTelemetryUseCliAuth').mockReturnValue(true);
|
||||
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Telemetry configuration error: "useCollector" and "useCliAuth" cannot both be true',
|
||||
),
|
||||
);
|
||||
expect(NodeSDK.prototype.start).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
|
||||
import {
|
||||
DiagLogLevel,
|
||||
diag,
|
||||
trace,
|
||||
context,
|
||||
metrics,
|
||||
propagation,
|
||||
} 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';
|
||||
@@ -28,6 +35,7 @@ import {
|
||||
PeriodicExportingMetricReader,
|
||||
} from '@opentelemetry/sdk-metrics';
|
||||
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
||||
import type { JWTInput } from 'google-auth-library';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { SERVICE_NAME } from './constants.js';
|
||||
import { initializeMetrics } from './metrics.js';
|
||||
@@ -44,17 +52,66 @@ import {
|
||||
} from './gcp-exporters.js';
|
||||
import { TelemetryTarget } from './index.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { authEvents } from '../code_assist/oauth2.js';
|
||||
|
||||
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
|
||||
class DiagLoggerAdapter {
|
||||
error(message: string, ...args: unknown[]): void {
|
||||
debugLogger.error(message, ...args);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: unknown[]): void {
|
||||
debugLogger.warn(message, ...args);
|
||||
}
|
||||
|
||||
info(message: string, ...args: unknown[]): void {
|
||||
debugLogger.log(message, ...args);
|
||||
}
|
||||
|
||||
debug(message: string, ...args: unknown[]): void {
|
||||
debugLogger.debug(message, ...args);
|
||||
}
|
||||
|
||||
verbose(message: string, ...args: unknown[]): void {
|
||||
debugLogger.debug(message, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
diag.setLogger(new DiagLoggerAdapter(), DiagLogLevel.INFO);
|
||||
|
||||
let sdk: NodeSDK | undefined;
|
||||
let telemetryInitialized = false;
|
||||
let callbackRegistered = false;
|
||||
let authListener: ((newCredentials: JWTInput) => Promise<void>) | undefined =
|
||||
undefined;
|
||||
const telemetryBuffer: Array<() => void | Promise<void>> = [];
|
||||
|
||||
export function isTelemetrySdkInitialized(): boolean {
|
||||
return telemetryInitialized;
|
||||
}
|
||||
|
||||
export function bufferTelemetryEvent(fn: () => void | Promise<void>): void {
|
||||
if (telemetryInitialized) {
|
||||
fn();
|
||||
} else {
|
||||
telemetryBuffer.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
async function flushTelemetryBuffer(): Promise<void> {
|
||||
if (!telemetryInitialized) return;
|
||||
while (telemetryBuffer.length > 0) {
|
||||
const fn = telemetryBuffer.shift();
|
||||
if (fn) {
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
debugLogger.error('Error executing buffered telemetry event', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseOtlpEndpoint(
|
||||
otlpEndpointSetting: string | undefined,
|
||||
protocol: 'grpc' | 'http',
|
||||
@@ -80,11 +137,46 @@ function parseOtlpEndpoint(
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeTelemetry(config: Config): void {
|
||||
export async function initializeTelemetry(
|
||||
config: Config,
|
||||
credentials?: JWTInput,
|
||||
): Promise<void> {
|
||||
if (telemetryInitialized || !config.getTelemetryEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.getTelemetryUseCollector() && config.getTelemetryUseCliAuth()) {
|
||||
debugLogger.error(
|
||||
'Telemetry configuration error: "useCollector" and "useCliAuth" cannot both be true. ' +
|
||||
'CLI authentication is only supported with in-process exporters. ' +
|
||||
'Disabling telemetry.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If using CLI auth and no credentials provided, defer initialization
|
||||
if (config.getTelemetryUseCliAuth() && !credentials) {
|
||||
// Register a callback to initialize telemetry when the user logs in.
|
||||
// This is done only once.
|
||||
if (!callbackRegistered) {
|
||||
callbackRegistered = true;
|
||||
authListener = async (newCredentials: JWTInput) => {
|
||||
if (config.getTelemetryEnabled() && config.getTelemetryUseCliAuth()) {
|
||||
debugLogger.log(
|
||||
'Telemetry reinit with credentials: ',
|
||||
newCredentials,
|
||||
);
|
||||
await initializeTelemetry(config, newCredentials);
|
||||
}
|
||||
};
|
||||
authEvents.on('post_auth', authListener);
|
||||
}
|
||||
debugLogger.log(
|
||||
'CLI auth is requested but no credentials, deferring telemetry initialization.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const resource = resourceFromAttributes({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: process.version,
|
||||
@@ -95,6 +187,7 @@ export function initializeTelemetry(config: Config): void {
|
||||
const otlpProtocol = config.getTelemetryOtlpProtocol();
|
||||
const telemetryTarget = config.getTelemetryTarget();
|
||||
const useCollector = config.getTelemetryUseCollector();
|
||||
|
||||
const parsedEndpoint = parseOtlpEndpoint(otlpEndpoint, otlpProtocol);
|
||||
const telemetryOutfile = config.getTelemetryOutfile();
|
||||
const useOtlp = !!parsedEndpoint && !telemetryOutfile;
|
||||
@@ -103,7 +196,7 @@ export function initializeTelemetry(config: Config): void {
|
||||
process.env['OTLP_GOOGLE_CLOUD_PROJECT'] ||
|
||||
process.env['GOOGLE_CLOUD_PROJECT'];
|
||||
const useDirectGcpExport =
|
||||
telemetryTarget === TelemetryTarget.GCP && !!gcpProjectId && !useCollector;
|
||||
telemetryTarget === TelemetryTarget.GCP && !useCollector;
|
||||
|
||||
let spanExporter:
|
||||
| OTLPTraceExporter
|
||||
@@ -120,10 +213,16 @@ export function initializeTelemetry(config: Config): void {
|
||||
let metricReader: PeriodicExportingMetricReader;
|
||||
|
||||
if (useDirectGcpExport) {
|
||||
spanExporter = new GcpTraceExporter(gcpProjectId);
|
||||
logExporter = new GcpLogExporter(gcpProjectId);
|
||||
debugLogger.log(
|
||||
'Creating GCP exporters with projectId:',
|
||||
gcpProjectId,
|
||||
'using',
|
||||
credentials ? 'provided credentials' : 'ADC',
|
||||
);
|
||||
spanExporter = new GcpTraceExporter(gcpProjectId, credentials);
|
||||
logExporter = new GcpLogExporter(gcpProjectId, credentials);
|
||||
metricReader = new PeriodicExportingMetricReader({
|
||||
exporter: new GcpMetricExporter(gcpProjectId),
|
||||
exporter: new GcpMetricExporter(gcpProjectId, credentials),
|
||||
exportIntervalMillis: 30000,
|
||||
});
|
||||
} else if (useOtlp) {
|
||||
@@ -183,12 +282,13 @@ export function initializeTelemetry(config: Config): void {
|
||||
});
|
||||
|
||||
try {
|
||||
sdk.start();
|
||||
await sdk.start();
|
||||
if (config.getDebugMode()) {
|
||||
debugLogger.log('OpenTelemetry SDK started successfully.');
|
||||
}
|
||||
telemetryInitialized = true;
|
||||
initializeMetrics(config);
|
||||
void flushTelemetryBuffer();
|
||||
} catch (error) {
|
||||
console.error('Error starting OpenTelemetry SDK:', error);
|
||||
}
|
||||
@@ -204,19 +304,36 @@ export function initializeTelemetry(config: Config): void {
|
||||
});
|
||||
}
|
||||
|
||||
export async function shutdownTelemetry(config: Config): Promise<void> {
|
||||
export async function shutdownTelemetry(
|
||||
config: Config,
|
||||
fromProcessExit = true,
|
||||
): Promise<void> {
|
||||
if (!telemetryInitialized || !sdk) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ClearcutLogger.getInstance()?.shutdown();
|
||||
await ClearcutLogger.getInstance()?.shutdown();
|
||||
await sdk.shutdown();
|
||||
if (config.getDebugMode()) {
|
||||
if (config.getDebugMode() && fromProcessExit) {
|
||||
debugLogger.log('OpenTelemetry SDK shut down successfully.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error shutting down SDK:', error);
|
||||
} finally {
|
||||
telemetryInitialized = false;
|
||||
sdk = undefined;
|
||||
// Fully reset the global APIs to allow for re-initialization.
|
||||
// This is primarily for testing environments where the SDK is started
|
||||
// and stopped multiple times in the same process.
|
||||
trace.disable();
|
||||
context.disable();
|
||||
metrics.disable();
|
||||
propagation.disable();
|
||||
diag.disable();
|
||||
if (authListener) {
|
||||
authEvents.off('post_auth', authListener);
|
||||
authListener = undefined;
|
||||
}
|
||||
callbackRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ vi.mock('node:os', () => ({
|
||||
// Mock fs module
|
||||
vi.mock('node:fs', () => ({
|
||||
existsSync: vi.fn(() => false),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('StartupProfiler', () => {
|
||||
|
||||
@@ -12,16 +12,26 @@ import {
|
||||
} from './sdk.js';
|
||||
import { Config } from '../config/config.js';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
|
||||
vi.mock('@opentelemetry/sdk-node');
|
||||
vi.mock('../config/config.js');
|
||||
vi.mock('google-auth-library');
|
||||
|
||||
describe('telemetry', () => {
|
||||
let mockConfig: Config;
|
||||
let mockNodeSdk: NodeSDK;
|
||||
const mockGetApplicationDefault = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(GoogleAuth).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getApplicationDefault: mockGetApplicationDefault,
|
||||
}) as unknown as GoogleAuth,
|
||||
);
|
||||
mockGetApplicationDefault.mockResolvedValue(undefined); // Simulate ADC available
|
||||
|
||||
mockConfig = new Config({
|
||||
sessionId: 'test-session-id',
|
||||
@@ -49,14 +59,14 @@ describe('telemetry', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize the telemetry service', () => {
|
||||
initializeTelemetry(mockConfig);
|
||||
it('should initialize the telemetry service', async () => {
|
||||
await initializeTelemetry(mockConfig);
|
||||
expect(NodeSDK).toHaveBeenCalled();
|
||||
expect(mockNodeSdk.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should shutdown the telemetry service', async () => {
|
||||
initializeTelemetry(mockConfig);
|
||||
await initializeTelemetry(mockConfig);
|
||||
await shutdownTelemetry(mockConfig);
|
||||
|
||||
expect(mockNodeSdk.shutdown).toHaveBeenCalled();
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as util from 'node:util';
|
||||
|
||||
/**
|
||||
* A simple, centralized logger for developer-facing debug messages.
|
||||
*
|
||||
@@ -17,19 +20,47 @@
|
||||
* will intercept these calls and route them to the debug drawer UI.
|
||||
*/
|
||||
class DebugLogger {
|
||||
private logStream: fs.WriteStream | undefined;
|
||||
|
||||
constructor() {
|
||||
this.logStream = process.env['GEMINI_DEBUG_LOG_FILE']
|
||||
? fs.createWriteStream(process.env['GEMINI_DEBUG_LOG_FILE'], {
|
||||
flags: 'a',
|
||||
})
|
||||
: undefined;
|
||||
// Handle potential errors with the stream
|
||||
this.logStream?.on('error', (err) => {
|
||||
// Log to console as a fallback, but don't crash the app
|
||||
console.error('Error writing to debug log stream:', err);
|
||||
});
|
||||
}
|
||||
|
||||
private writeToFile(level: string, args: unknown[]) {
|
||||
if (this.logStream) {
|
||||
const message = util.format(...args);
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
|
||||
this.logStream.write(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
log(...args: unknown[]): void {
|
||||
this.writeToFile('LOG', args);
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
warn(...args: unknown[]): void {
|
||||
this.writeToFile('WARN', args);
|
||||
console.warn(...args);
|
||||
}
|
||||
|
||||
error(...args: unknown[]): void {
|
||||
this.writeToFile('ERROR', args);
|
||||
console.error(...args);
|
||||
}
|
||||
|
||||
debug(...args: unknown[]): void {
|
||||
this.writeToFile('DEBUG', args);
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ let mockSendMessageStream: any;
|
||||
vi.mock('fs', () => ({
|
||||
statSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../core/client.js', () => ({
|
||||
|
||||
@@ -32,6 +32,10 @@ vi.mock('node:fs', () => {
|
||||
});
|
||||
}),
|
||||
existsSync: vi.fn((path: string) => mockFileSystem.has(path)),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -20,15 +20,15 @@ const USER_SETTINGS_DIR = join(
|
||||
const USER_SETTINGS_PATH = join(USER_SETTINGS_DIR, 'settings.json');
|
||||
const WORKSPACE_SETTINGS_PATH = join(projectRoot, GEMINI_DIR, 'settings.json');
|
||||
|
||||
let settingsTarget = undefined;
|
||||
let telemetrySettings = undefined;
|
||||
|
||||
function loadSettingsValue(filePath) {
|
||||
function loadSettings(filePath) {
|
||||
try {
|
||||
if (existsSync(filePath)) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const jsonContent = content.replace(/\/\/[^\n]*/g, '');
|
||||
const settings = JSON.parse(jsonContent);
|
||||
return settings.telemetry?.target;
|
||||
return settings.telemetry;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
@@ -38,13 +38,13 @@ function loadSettingsValue(filePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
settingsTarget = loadSettingsValue(WORKSPACE_SETTINGS_PATH);
|
||||
telemetrySettings = loadSettings(WORKSPACE_SETTINGS_PATH);
|
||||
|
||||
if (!settingsTarget) {
|
||||
settingsTarget = loadSettingsValue(USER_SETTINGS_PATH);
|
||||
if (!telemetrySettings) {
|
||||
telemetrySettings = loadSettings(USER_SETTINGS_PATH);
|
||||
}
|
||||
|
||||
let target = settingsTarget || 'local';
|
||||
let target = telemetrySettings?.target || 'local';
|
||||
const allowedTargets = ['local', 'gcp', 'genkit'];
|
||||
|
||||
const targetArg = process.argv.find((arg) => arg.startsWith('--target='));
|
||||
@@ -55,13 +55,15 @@ if (targetArg) {
|
||||
console.log(`⚙️ Using command-line target: ${target}`);
|
||||
} else {
|
||||
console.error(
|
||||
`🛑 Error: Invalid target '${potentialTarget}'. Allowed targets are: ${allowedTargets.join(', ')}.`,
|
||||
`🛑 Error: Invalid target '${potentialTarget}'. Allowed targets are: ${allowedTargets.join(
|
||||
', ',
|
||||
)}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (settingsTarget) {
|
||||
} else if (telemetrySettings?.target) {
|
||||
console.log(
|
||||
`⚙️ Using telemetry target from settings.json: ${settingsTarget}`,
|
||||
`⚙️ Using telemetry target from settings.json: ${telemetrySettings.target}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +77,13 @@ const scriptPath = join(projectRoot, 'scripts', targetScripts[target]);
|
||||
|
||||
try {
|
||||
console.log(`🚀 Running telemetry script for target: ${target}.`);
|
||||
execSync(`node ${scriptPath}`, { stdio: 'inherit', cwd: projectRoot });
|
||||
const env = { ...process.env };
|
||||
|
||||
execSync(`node ${scriptPath}`, {
|
||||
stdio: 'inherit',
|
||||
cwd: projectRoot,
|
||||
env,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`🛑 Failed to run telemetry script for target: ${target}`);
|
||||
console.error(error);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import * as fs from 'node:fs';
|
||||
import { spawn, execSync } from 'node:child_process';
|
||||
import {
|
||||
OTEL_DIR,
|
||||
@@ -132,11 +132,13 @@ async function main() {
|
||||
fs.writeFileSync(OTEL_CONFIG_FILE, getOtelConfigContent(projectId));
|
||||
console.log(`📄 Wrote OTEL collector config to ${OTEL_CONFIG_FILE}`);
|
||||
|
||||
const spawnEnv = { ...process.env };
|
||||
|
||||
console.log(`🚀 Starting OTEL collector for GCP... Logs: ${OTEL_LOG_FILE}`);
|
||||
collectorLogFd = fs.openSync(OTEL_LOG_FILE, 'a');
|
||||
collectorProcess = spawn(otelcolPath, ['--config', OTEL_CONFIG_FILE], {
|
||||
stdio: ['ignore', collectorLogFd, collectorLogFd],
|
||||
env: { ...process.env },
|
||||
env: spawnEnv,
|
||||
});
|
||||
|
||||
console.log(
|
||||
|
||||
@@ -4,9 +4,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { main as generateDocs } from '../generate-settings-doc.ts';
|
||||
|
||||
vi.mock('fs', () => ({
|
||||
readFileSync: vi.fn().mockReturnValue(''),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('generate-settings-doc', () => {
|
||||
it('keeps documentation in sync in check mode', async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
|
||||
@@ -4,12 +4,21 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { main as generateSchema } from '../generate-settings-schema.ts';
|
||||
|
||||
vi.mock('fs', () => ({
|
||||
readFileSync: vi.fn().mockReturnValue(''),
|
||||
writeFileSync: vi.fn(),
|
||||
createWriteStream: vi.fn(() => ({
|
||||
write: vi.fn(),
|
||||
on: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('generate-settings-schema', () => {
|
||||
it('keeps schema in sync in check mode', async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
|
||||
56
scripts/tests/telemetry_gcp.test.ts
Normal file
56
scripts/tests/telemetry_gcp.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
const mockSpawn = vi.fn(() => ({ on: vi.fn(), pid: 123 }));
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
spawn: mockSpawn,
|
||||
execSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
readFileSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
openSync: vi.fn(() => 1),
|
||||
unlinkSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../telemetry_utils.js', () => ({
|
||||
ensureBinary: vi.fn(() => Promise.resolve('/fake/path/to/otelcol-contrib')),
|
||||
waitForPort: vi.fn(() => Promise.resolve()),
|
||||
manageTelemetrySettings: vi.fn(),
|
||||
registerCleanup: vi.fn(),
|
||||
fileExists: vi.fn(() => true), // Assume all files exist for simplicity
|
||||
OTEL_DIR: '/tmp/otel',
|
||||
BIN_DIR: '/tmp/bin',
|
||||
}));
|
||||
|
||||
describe('telemetry_gcp.js', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules(); // This is key to re-run the script
|
||||
vi.clearAllMocks();
|
||||
process.env.OTLP_GOOGLE_CLOUD_PROJECT = 'test-project';
|
||||
// Clear the env var before each test
|
||||
delete process.env.GEMINI_CLI_CREDENTIALS_PATH;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.OTLP_GOOGLE_CLOUD_PROJECT;
|
||||
});
|
||||
|
||||
it('should not set GOOGLE_APPLICATION_CREDENTIALS when env var is not set', async () => {
|
||||
await import('../telemetry_gcp.js');
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalled();
|
||||
const spawnOptions = mockSpawn.mock.calls[0][2];
|
||||
expect(spawnOptions?.env).not.toHaveProperty(
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user