mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
import * as https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
|
||||
import {
|
||||
StartSessionEvent,
|
||||
EndSessionEvent,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
ApiErrorEvent,
|
||||
FlashFallbackEvent,
|
||||
LoopDetectedEvent,
|
||||
FlashDecidedToContinueEvent,
|
||||
} from '../types.js';
|
||||
import { EventMetadataKey } from './event-metadata-key.js';
|
||||
import { Config } from '../../config/config.js';
|
||||
@@ -35,6 +38,7 @@ const api_error_event_name = 'api_error';
|
||||
const end_session_event_name = 'end_session';
|
||||
const flash_fallback_event_name = 'flash_fallback';
|
||||
const loop_detected_event_name = 'loop_detected';
|
||||
const flash_decided_to_continue_event_name = 'flash_decided_to_continue';
|
||||
|
||||
export interface LogResponse {
|
||||
nextRequestWaitMs?: number;
|
||||
@@ -132,12 +136,18 @@ export class ClearcutLogger {
|
||||
headers: { 'Content-Length': Buffer.byteLength(body) },
|
||||
};
|
||||
const bufs: Buffer[] = [];
|
||||
const req = https.request(options, (res) => {
|
||||
res.on('data', (buf) => bufs.push(buf));
|
||||
res.on('end', () => {
|
||||
resolve(Buffer.concat(bufs));
|
||||
});
|
||||
});
|
||||
const req = https.request(
|
||||
{
|
||||
...options,
|
||||
agent: this.getProxyAgent(),
|
||||
},
|
||||
(res) => {
|
||||
res.on('data', (buf) => bufs.push(buf));
|
||||
res.on('end', () => {
|
||||
resolve(Buffer.concat(bufs));
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on('error', (e) => {
|
||||
if (this.config?.getDebugMode()) {
|
||||
console.log('Clearcut POST request error: ', e);
|
||||
@@ -205,11 +215,16 @@ export class ClearcutLogger {
|
||||
}
|
||||
|
||||
logStartSessionEvent(event: StartSessionEvent): void {
|
||||
const surface = process.env.SURFACE || 'SURFACE_NOT_SET';
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL,
|
||||
value: event.model,
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
|
||||
value: this.config?.getSessionId() ?? '',
|
||||
},
|
||||
{
|
||||
gemini_cli_key:
|
||||
EventMetadataKey.GEMINI_CLI_START_SESSION_EMBEDDING_MODEL,
|
||||
@@ -266,7 +281,12 @@ export class ClearcutLogger {
|
||||
EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED,
|
||||
value: event.telemetry_log_user_prompts_enabled.toString(),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
|
||||
value: surface,
|
||||
},
|
||||
];
|
||||
|
||||
// Flush start event immediately
|
||||
this.enqueueLogEvent(this.createLogEvent(start_session_event_name, data));
|
||||
this.flushToClearcut().catch((error) => {
|
||||
@@ -280,6 +300,10 @@ export class ClearcutLogger {
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_PROMPT_LENGTH,
|
||||
value: JSON.stringify(event.prompt_length),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
|
||||
value: this.config?.getSessionId() ?? '',
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
|
||||
value: JSON.stringify(event.prompt_id),
|
||||
@@ -442,6 +466,10 @@ export class ClearcutLogger {
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
|
||||
value: JSON.stringify(event.auth_type),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
|
||||
value: this.config?.getSessionId() ?? '',
|
||||
},
|
||||
];
|
||||
|
||||
this.enqueueLogEvent(this.createLogEvent(flash_fallback_event_name, data));
|
||||
@@ -452,6 +480,10 @@ export class ClearcutLogger {
|
||||
|
||||
logLoopDetectedEvent(event: LoopDetectedEvent): void {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
|
||||
value: JSON.stringify(event.prompt_id),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_TYPE,
|
||||
value: JSON.stringify(event.loop_type),
|
||||
@@ -462,10 +494,28 @@ export class ClearcutLogger {
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logFlashDecidedToContinueEvent(event: FlashDecidedToContinueEvent): void {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
|
||||
value: JSON.stringify(event.prompt_id),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
|
||||
value: this.config?.getSessionId() ?? '',
|
||||
},
|
||||
];
|
||||
|
||||
this.enqueueLogEvent(
|
||||
this.createLogEvent(flash_decided_to_continue_event_name, data),
|
||||
);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logEndSessionEvent(event: EndSessionEvent): void {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_END_SESSION_ID,
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
|
||||
value: event?.session_id?.toString() ?? '',
|
||||
},
|
||||
];
|
||||
@@ -477,6 +527,18 @@ export class ClearcutLogger {
|
||||
});
|
||||
}
|
||||
|
||||
getProxyAgent() {
|
||||
const proxyUrl = this.config?.getProxy();
|
||||
if (!proxyUrl) return undefined;
|
||||
// undici which is widely used in the repo can only support http & https proxy protocol,
|
||||
// https://github.com/nodejs/undici/issues/2224
|
||||
if (proxyUrl.startsWith('http')) {
|
||||
return new HttpsProxyAgent(proxyUrl);
|
||||
} else {
|
||||
throw new Error('Unsupported proxy type');
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
const event = new EndSessionEvent(this.config);
|
||||
this.logEndSessionEvent(event);
|
||||
|
||||
@@ -151,6 +151,12 @@ export enum EventMetadataKey {
|
||||
// Logs the total number of Google accounts ever used.
|
||||
GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT = 37,
|
||||
|
||||
// Logs the Surface from where the Gemini CLI was invoked, eg: VSCode.
|
||||
GEMINI_CLI_SURFACE = 39,
|
||||
|
||||
// Logs the session id
|
||||
GEMINI_CLI_SESSION_ID = 40,
|
||||
|
||||
// ==========================================================================
|
||||
// Loop Detected Event Keys
|
||||
// ===========================================================================
|
||||
|
||||
@@ -13,7 +13,8 @@ export const EVENT_API_ERROR = 'qwen-code.api_error';
|
||||
export const EVENT_API_RESPONSE = 'qwen-code.api_response';
|
||||
export const EVENT_CLI_CONFIG = 'qwen-code.config';
|
||||
export const EVENT_FLASH_FALLBACK = 'qwen-code.flash_fallback';
|
||||
|
||||
export const EVENT_FLASH_DECIDED_TO_CONTINUE =
|
||||
'qwen-code.flash_decided_to_continue';
|
||||
export const METRIC_TOOL_CALL_COUNT = 'qwen-code.tool.call.count';
|
||||
export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency';
|
||||
export const METRIC_API_REQUEST_COUNT = 'qwen-code.api.request.count';
|
||||
|
||||
89
packages/core/src/telemetry/file-exporters.ts
Normal file
89
packages/core/src/telemetry/file-exporters.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
|
||||
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
||||
import { ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs';
|
||||
import {
|
||||
ResourceMetrics,
|
||||
PushMetricExporter,
|
||||
AggregationTemporality,
|
||||
} from '@opentelemetry/sdk-metrics';
|
||||
|
||||
class FileExporter {
|
||||
protected writeStream: fs.WriteStream;
|
||||
|
||||
constructor(filePath: string) {
|
||||
this.writeStream = fs.createWriteStream(filePath, { flags: 'a' });
|
||||
}
|
||||
|
||||
protected serialize(data: unknown): string {
|
||||
return JSON.stringify(data, null, 2) + '\n';
|
||||
}
|
||||
|
||||
shutdown(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.writeStream.end(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileSpanExporter extends FileExporter implements SpanExporter {
|
||||
export(
|
||||
spans: ReadableSpan[],
|
||||
resultCallback: (result: ExportResult) => void,
|
||||
): void {
|
||||
const data = spans.map((span) => this.serialize(span)).join('');
|
||||
this.writeStream.write(data, (err) => {
|
||||
resultCallback({
|
||||
code: err ? ExportResultCode.FAILED : ExportResultCode.SUCCESS,
|
||||
error: err || undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileLogExporter extends FileExporter implements LogRecordExporter {
|
||||
export(
|
||||
logs: ReadableLogRecord[],
|
||||
resultCallback: (result: ExportResult) => void,
|
||||
): void {
|
||||
const data = logs.map((log) => this.serialize(log)).join('');
|
||||
this.writeStream.write(data, (err) => {
|
||||
resultCallback({
|
||||
code: err ? ExportResultCode.FAILED : ExportResultCode.SUCCESS,
|
||||
error: err || undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileMetricExporter
|
||||
extends FileExporter
|
||||
implements PushMetricExporter
|
||||
{
|
||||
export(
|
||||
metrics: ResourceMetrics,
|
||||
resultCallback: (result: ExportResult) => void,
|
||||
): void {
|
||||
const data = this.serialize(metrics);
|
||||
this.writeStream.write(data, (err) => {
|
||||
resultCallback({
|
||||
code: err ? ExportResultCode.FAILED : ExportResultCode.SUCCESS,
|
||||
error: err || undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getPreferredAggregationTemporality(): AggregationTemporality {
|
||||
return AggregationTemporality.CUMULATIVE;
|
||||
}
|
||||
|
||||
async forceFlush(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
export enum TelemetryTarget {
|
||||
GCP = 'gcp',
|
||||
QW = 'qw',
|
||||
LOCAL = 'local',
|
||||
QWEN = 'qwen',
|
||||
}
|
||||
|
||||
const DEFAULT_TELEMETRY_TARGET = TelemetryTarget.QW;
|
||||
const DEFAULT_OTLP_ENDPOINT = 'http://tracing-analysis-dc-hz.aliyuncs.com:8090';
|
||||
const DEFAULT_TELEMETRY_TARGET = TelemetryTarget.LOCAL;
|
||||
const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4317';
|
||||
|
||||
export { DEFAULT_TELEMETRY_TARGET, DEFAULT_OTLP_ENDPOINT };
|
||||
export {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import { logs, LogRecord, LogAttributes } from '@opentelemetry/api-logs';
|
||||
import { trace, context } from '@opentelemetry/api';
|
||||
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { Config } from '../config/config.js';
|
||||
import {
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
EVENT_TOOL_CALL,
|
||||
EVENT_USER_PROMPT,
|
||||
EVENT_FLASH_FALLBACK,
|
||||
EVENT_FLASH_DECIDED_TO_CONTINUE,
|
||||
SERVICE_NAME,
|
||||
} from './constants.js';
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
ToolCallEvent,
|
||||
UserPromptEvent,
|
||||
FlashFallbackEvent,
|
||||
FlashDecidedToContinueEvent,
|
||||
LoopDetectedEvent,
|
||||
} from './types.js';
|
||||
import {
|
||||
@@ -36,7 +37,7 @@ import {
|
||||
} from './metrics.js';
|
||||
import { isTelemetrySdkInitialized } from './sdk.js';
|
||||
import { uiTelemetryService, UiEvent } from './uiTelemetry.js';
|
||||
// import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
|
||||
const shouldLogUserPrompts = (config: Config): boolean =>
|
||||
@@ -48,32 +49,11 @@ function getCommonAttributes(config: Config): LogAttributes {
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create spans and emit logs within span context
|
||||
function logWithSpan(
|
||||
spanName: string,
|
||||
logBody: string,
|
||||
attributes: LogAttributes,
|
||||
): void {
|
||||
const tracer = trace.getTracer(SERVICE_NAME);
|
||||
const span = tracer.startSpan(spanName);
|
||||
|
||||
context.with(trace.setSpan(context.active(), span), () => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: logBody,
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
});
|
||||
|
||||
span.end();
|
||||
}
|
||||
|
||||
export function logCliConfiguration(
|
||||
config: Config,
|
||||
event: StartSessionEvent,
|
||||
): void {
|
||||
// ClearcutLogger.getInstance(config)?.logStartSessionEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logStartSessionEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -93,11 +73,16 @@ export function logCliConfiguration(
|
||||
mcp_servers: event.mcp_servers,
|
||||
};
|
||||
|
||||
logWithSpan('cli.configuration', 'CLI configuration loaded.', attributes);
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: 'CLI configuration loaded.',
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logUserPrompt(config: Config, event: UserPromptEvent): void {
|
||||
// ClearcutLogger.getInstance(config)?.logNewPromptEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logNewPromptEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -111,11 +96,12 @@ export function logUserPrompt(config: Config, event: UserPromptEvent): void {
|
||||
attributes.prompt = event.prompt;
|
||||
}
|
||||
|
||||
logWithSpan(
|
||||
'user.prompt',
|
||||
`User prompt. Length: ${event.prompt_length}.`,
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `User prompt. Length: ${event.prompt_length}.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
@@ -125,7 +111,7 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
// ClearcutLogger.getInstance(config)?.logToolCallEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logToolCallEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -142,11 +128,12 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
logWithSpan(
|
||||
`tool.${event.function_name}`,
|
||||
`Tool call: ${event.function_name}${event.decision ? `. Decision: ${event.decision}` : ''}. Success: ${event.success}. Duration: ${event.duration_ms}ms.`,
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Tool call: ${event.function_name}${event.decision ? `. Decision: ${event.decision}` : ''}. Success: ${event.success}. Duration: ${event.duration_ms}ms.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordToolCallMetrics(
|
||||
config,
|
||||
event.function_name,
|
||||
@@ -157,7 +144,7 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
|
||||
}
|
||||
|
||||
export function logApiRequest(config: Config, event: ApiRequestEvent): void {
|
||||
// ClearcutLogger.getInstance(config)?.logApiRequestEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logApiRequestEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -167,18 +154,19 @@ export function logApiRequest(config: Config, event: ApiRequestEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
};
|
||||
|
||||
logWithSpan(
|
||||
`api.request.${event.model}`,
|
||||
`API request to ${event.model}.`,
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `API request to ${event.model}.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logFlashFallback(
|
||||
config: Config,
|
||||
event: FlashFallbackEvent,
|
||||
): void {
|
||||
// ClearcutLogger.getInstance(config)?.logFlashFallbackEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logFlashFallbackEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -188,11 +176,12 @@ export function logFlashFallback(
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
};
|
||||
|
||||
logWithSpan(
|
||||
'api.flash_fallback',
|
||||
'Switching to flash as Fallback.',
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Switching to flash as Fallback.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logApiError(config: Config, event: ApiErrorEvent): void {
|
||||
@@ -202,7 +191,7 @@ export function logApiError(config: Config, event: ApiErrorEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
// ClearcutLogger.getInstance(config)?.logApiErrorEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logApiErrorEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -222,11 +211,12 @@ export function logApiError(config: Config, event: ApiErrorEvent): void {
|
||||
attributes[SemanticAttributes.HTTP_STATUS_CODE] = event.status_code;
|
||||
}
|
||||
|
||||
logWithSpan(
|
||||
`api.error.${event.model}`,
|
||||
`API error for ${event.model}. Error: ${event.error}. Duration: ${event.duration_ms}ms.`,
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `API error for ${event.model}. Error: ${event.error}. Duration: ${event.duration_ms}ms.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordApiErrorMetrics(
|
||||
config,
|
||||
event.model,
|
||||
@@ -243,7 +233,7 @@ export function logApiResponse(config: Config, event: ApiResponseEvent): void {
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
} as UiEvent;
|
||||
uiTelemetryService.addEvent(uiEvent);
|
||||
// ClearcutLogger.getInstance(config)?.logApiResponseEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logApiResponseEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
@@ -262,11 +252,12 @@ export function logApiResponse(config: Config, event: ApiResponseEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
logWithSpan(
|
||||
`api.response.${event.model}`,
|
||||
`API response from ${event.model}. Status: ${event.status_code || 'N/A'}. Duration: ${event.duration_ms}ms.`,
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `API response from ${event.model}. Status: ${event.status_code || 'N/A'}. Duration: ${event.duration_ms}ms.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
recordApiResponseMetrics(
|
||||
config,
|
||||
event.model,
|
||||
@@ -305,7 +296,7 @@ export function logLoopDetected(
|
||||
config: Config,
|
||||
event: LoopDetectedEvent,
|
||||
): void {
|
||||
// ClearcutLogger.getInstance(config)?.logLoopDetectedEvent(event);
|
||||
ClearcutLogger.getInstance(config)?.logLoopDetectedEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
@@ -313,9 +304,31 @@ export function logLoopDetected(
|
||||
...event,
|
||||
};
|
||||
|
||||
logWithSpan(
|
||||
'loop.detected',
|
||||
`Loop detected. Type: ${event.loop_type}.`,
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Loop detected. Type: ${event.loop_type}.`,
|
||||
attributes,
|
||||
);
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logFlashDecidedToContinue(
|
||||
config: Config,
|
||||
event: FlashDecidedToContinueEvent,
|
||||
): void {
|
||||
ClearcutLogger.getInstance(config)?.logFlashDecidedToContinueEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
...event,
|
||||
'event.name': EVENT_FLASH_DECIDED_TO_CONTINUE,
|
||||
};
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Flash decided to continue.`,
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
@@ -6,23 +6,34 @@
|
||||
|
||||
import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
|
||||
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
|
||||
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
|
||||
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
|
||||
import { Metadata } from '@grpc/grpc-js';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { Resource } from '@opentelemetry/resources';
|
||||
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
|
||||
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
||||
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
||||
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
||||
import type { LogRecord } from '@opentelemetry/sdk-logs';
|
||||
import type { ResourceMetrics } from '@opentelemetry/sdk-metrics';
|
||||
import type { ExportResult } from '@opentelemetry/core';
|
||||
import {
|
||||
BatchSpanProcessor,
|
||||
ConsoleSpanExporter,
|
||||
} from '@opentelemetry/sdk-trace-node';
|
||||
import {
|
||||
BatchLogRecordProcessor,
|
||||
ConsoleLogRecordExporter,
|
||||
} from '@opentelemetry/sdk-logs';
|
||||
import {
|
||||
ConsoleMetricExporter,
|
||||
PeriodicExportingMetricReader,
|
||||
} from '@opentelemetry/sdk-metrics';
|
||||
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
||||
import { Config } from '../config/config.js';
|
||||
import { SERVICE_NAME } from './constants.js';
|
||||
import { initializeMetrics } from './metrics.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import {
|
||||
FileLogExporter,
|
||||
FileMetricExporter,
|
||||
FileSpanExporter,
|
||||
} from './file-exporters.js';
|
||||
|
||||
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
|
||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
|
||||
@@ -68,63 +79,41 @@ export function initializeTelemetry(config: Config): void {
|
||||
const otlpEndpoint = config.getTelemetryOtlpEndpoint();
|
||||
const grpcParsedEndpoint = parseGrpcEndpoint(otlpEndpoint);
|
||||
const useOtlp = !!grpcParsedEndpoint;
|
||||
|
||||
const metadata = new Metadata();
|
||||
metadata.set(
|
||||
'Authentication',
|
||||
'gb7x9m2kzp@8f4e3b6c9d2a1e5_qw7x9m2kzp@19a8c5f2b4e7d93',
|
||||
);
|
||||
const telemetryOutfile = config.getTelemetryOutfile();
|
||||
|
||||
const spanExporter = useOtlp
|
||||
? new OTLPTraceExporter({
|
||||
url: grpcParsedEndpoint,
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
metadata,
|
||||
})
|
||||
: {
|
||||
export: (
|
||||
spans: ReadableSpan[],
|
||||
callback: (result: ExportResult) => void,
|
||||
) => callback({ code: 0 }),
|
||||
forceFlush: () => Promise.resolve(),
|
||||
shutdown: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
// FIXME: Temporarily disable OTLP log export due to gRPC endpoint not supporting LogsService
|
||||
// const logExporter = useOtlp
|
||||
// ? new OTLPLogExporter({
|
||||
// url: grpcParsedEndpoint,
|
||||
// compression: CompressionAlgorithm.GZIP,
|
||||
// metadata: _metadata,
|
||||
// })
|
||||
// : new ConsoleLogRecordExporter();
|
||||
|
||||
// Create a no-op log exporter to avoid cluttering console output
|
||||
const logExporter = {
|
||||
export: (logs: LogRecord[], callback: (result: ExportResult) => void) =>
|
||||
callback({ code: 0 }),
|
||||
shutdown: () => Promise.resolve(),
|
||||
};
|
||||
: telemetryOutfile
|
||||
? new FileSpanExporter(telemetryOutfile)
|
||||
: new ConsoleSpanExporter();
|
||||
const logExporter = useOtlp
|
||||
? new OTLPLogExporter({
|
||||
url: grpcParsedEndpoint,
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
})
|
||||
: telemetryOutfile
|
||||
? new FileLogExporter(telemetryOutfile)
|
||||
: new ConsoleLogRecordExporter();
|
||||
const metricReader = useOtlp
|
||||
? new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({
|
||||
url: grpcParsedEndpoint,
|
||||
compression: CompressionAlgorithm.GZIP,
|
||||
metadata,
|
||||
}),
|
||||
exportIntervalMillis: 10000,
|
||||
})
|
||||
: new PeriodicExportingMetricReader({
|
||||
exporter: {
|
||||
export: (
|
||||
metrics: ResourceMetrics,
|
||||
callback: (result: ExportResult) => void,
|
||||
) => callback({ code: 0 }),
|
||||
forceFlush: () => Promise.resolve(),
|
||||
shutdown: () => Promise.resolve(),
|
||||
},
|
||||
exportIntervalMillis: 10000,
|
||||
});
|
||||
: telemetryOutfile
|
||||
? new PeriodicExportingMetricReader({
|
||||
exporter: new FileMetricExporter(telemetryOutfile),
|
||||
exportIntervalMillis: 10000,
|
||||
})
|
||||
: new PeriodicExportingMetricReader({
|
||||
exporter: new ConsoleMetricExporter(),
|
||||
exportIntervalMillis: 10000,
|
||||
});
|
||||
|
||||
sdk = new NodeSDK({
|
||||
resource,
|
||||
@@ -152,7 +141,7 @@ export async function shutdownTelemetry(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// ClearcutLogger.getInstance()?.shutdown();
|
||||
ClearcutLogger.getInstance()?.shutdown();
|
||||
await sdk.shutdown();
|
||||
console.log('OpenTelemetry SDK shut down successfully.');
|
||||
} catch (error) {
|
||||
|
||||
@@ -249,17 +249,32 @@ export class FlashFallbackEvent {
|
||||
export enum LoopType {
|
||||
CONSECUTIVE_IDENTICAL_TOOL_CALLS = 'consecutive_identical_tool_calls',
|
||||
CHANTING_IDENTICAL_SENTENCES = 'chanting_identical_sentences',
|
||||
LLM_DETECTED_LOOP = 'llm_detected_loop',
|
||||
}
|
||||
|
||||
export class LoopDetectedEvent {
|
||||
'event.name': 'loop_detected';
|
||||
'event.timestamp': string; // ISO 8601
|
||||
loop_type: LoopType;
|
||||
prompt_id: string;
|
||||
|
||||
constructor(loop_type: LoopType) {
|
||||
constructor(loop_type: LoopType, prompt_id: string) {
|
||||
this['event.name'] = 'loop_detected';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.loop_type = loop_type;
|
||||
this.prompt_id = prompt_id;
|
||||
}
|
||||
}
|
||||
|
||||
export class FlashDecidedToContinueEvent {
|
||||
'event.name': 'flash_decided_to_continue';
|
||||
'event.timestamp': string; // ISO 8601
|
||||
prompt_id: string;
|
||||
|
||||
constructor(prompt_id: string) {
|
||||
this['event.name'] = 'flash_decided_to_continue';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.prompt_id = prompt_id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,4 +287,5 @@ export type TelemetryEvent =
|
||||
| ApiErrorEvent
|
||||
| ApiResponseEvent
|
||||
| FlashFallbackEvent
|
||||
| LoopDetectedEvent;
|
||||
| LoopDetectedEvent
|
||||
| FlashDecidedToContinueEvent;
|
||||
|
||||
@@ -508,4 +508,116 @@ describe('UiTelemetryService', () => {
|
||||
expect(tools.byName['tool_B'].count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetLastPromptTokenCount', () => {
|
||||
it('should reset the last prompt token count to 0', () => {
|
||||
// First, set up some initial token count
|
||||
const event = {
|
||||
'event.name': EVENT_API_RESPONSE,
|
||||
model: 'gemini-2.5-pro',
|
||||
duration_ms: 500,
|
||||
input_token_count: 100,
|
||||
output_token_count: 200,
|
||||
total_token_count: 300,
|
||||
cached_content_token_count: 50,
|
||||
thoughts_token_count: 20,
|
||||
tool_token_count: 30,
|
||||
} as ApiResponseEvent & { 'event.name': typeof EVENT_API_RESPONSE };
|
||||
|
||||
service.addEvent(event);
|
||||
expect(service.getLastPromptTokenCount()).toBe(100);
|
||||
|
||||
// Now reset the token count
|
||||
service.resetLastPromptTokenCount();
|
||||
expect(service.getLastPromptTokenCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should emit an update event when resetLastPromptTokenCount is called', () => {
|
||||
const spy = vi.fn();
|
||||
service.on('update', spy);
|
||||
|
||||
// Set up initial token count
|
||||
const event = {
|
||||
'event.name': EVENT_API_RESPONSE,
|
||||
model: 'gemini-2.5-pro',
|
||||
duration_ms: 500,
|
||||
input_token_count: 100,
|
||||
output_token_count: 200,
|
||||
total_token_count: 300,
|
||||
cached_content_token_count: 50,
|
||||
thoughts_token_count: 20,
|
||||
tool_token_count: 30,
|
||||
} as ApiResponseEvent & { 'event.name': typeof EVENT_API_RESPONSE };
|
||||
|
||||
service.addEvent(event);
|
||||
spy.mockClear(); // Clear the spy to focus on the reset call
|
||||
|
||||
service.resetLastPromptTokenCount();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
const { metrics, lastPromptTokenCount } = spy.mock.calls[0][0];
|
||||
expect(metrics).toBeDefined();
|
||||
expect(lastPromptTokenCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should not affect other metrics when resetLastPromptTokenCount is called', () => {
|
||||
// Set up initial state with some metrics
|
||||
const event = {
|
||||
'event.name': EVENT_API_RESPONSE,
|
||||
model: 'gemini-2.5-pro',
|
||||
duration_ms: 500,
|
||||
input_token_count: 100,
|
||||
output_token_count: 200,
|
||||
total_token_count: 300,
|
||||
cached_content_token_count: 50,
|
||||
thoughts_token_count: 20,
|
||||
tool_token_count: 30,
|
||||
} as ApiResponseEvent & { 'event.name': typeof EVENT_API_RESPONSE };
|
||||
|
||||
service.addEvent(event);
|
||||
|
||||
const metricsBefore = service.getMetrics();
|
||||
|
||||
service.resetLastPromptTokenCount();
|
||||
|
||||
const metricsAfter = service.getMetrics();
|
||||
|
||||
// Metrics should be unchanged
|
||||
expect(metricsAfter).toEqual(metricsBefore);
|
||||
|
||||
// Only the last prompt token count should be reset
|
||||
expect(service.getLastPromptTokenCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should work correctly when called multiple times', () => {
|
||||
const spy = vi.fn();
|
||||
service.on('update', spy);
|
||||
|
||||
// Set up initial token count
|
||||
const event = {
|
||||
'event.name': EVENT_API_RESPONSE,
|
||||
model: 'gemini-2.5-pro',
|
||||
duration_ms: 500,
|
||||
input_token_count: 100,
|
||||
output_token_count: 200,
|
||||
total_token_count: 300,
|
||||
cached_content_token_count: 50,
|
||||
thoughts_token_count: 20,
|
||||
tool_token_count: 30,
|
||||
} as ApiResponseEvent & { 'event.name': typeof EVENT_API_RESPONSE };
|
||||
|
||||
service.addEvent(event);
|
||||
expect(service.getLastPromptTokenCount()).toBe(100);
|
||||
|
||||
// Reset once
|
||||
service.resetLastPromptTokenCount();
|
||||
expect(service.getLastPromptTokenCount()).toBe(0);
|
||||
|
||||
// Reset again - should still be 0 and still emit event
|
||||
spy.mockClear();
|
||||
service.resetLastPromptTokenCount();
|
||||
expect(service.getLastPromptTokenCount()).toBe(0);
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,6 +133,14 @@ export class UiTelemetryService extends EventEmitter {
|
||||
return this.#lastPromptTokenCount;
|
||||
}
|
||||
|
||||
resetLastPromptTokenCount(): void {
|
||||
this.#lastPromptTokenCount = 0;
|
||||
this.emit('update', {
|
||||
metrics: this.#metrics,
|
||||
lastPromptTokenCount: this.#lastPromptTokenCount,
|
||||
});
|
||||
}
|
||||
|
||||
private getOrCreateModelMetrics(modelName: string): ModelMetrics {
|
||||
if (!this.#metrics.models[modelName]) {
|
||||
this.#metrics.models[modelName] = createInitialModelMetrics();
|
||||
|
||||
Reference in New Issue
Block a user