Refactor OTEL logging for API calls (#991)

Refactor OpenTelemetry logging for API requests, responses, and errors. Moved logging responsibility from GeminiClient to GeminiChat for more detailed logging.

#750
This commit is contained in:
Jerop Kipruto
2025-06-12 19:36:51 -04:00
committed by GitHub
parent dc378e8d60
commit 3c3da655b0
6 changed files with 294 additions and 209 deletions

View File

@@ -27,11 +27,6 @@ import { GeminiChat } from './geminiChat.js';
import { retryWithBackoff } from '../utils/retry.js';
import { getErrorMessage } from '../utils/errors.js';
import { tokenLimit } from './tokenLimits.js';
import {
logApiRequest,
logApiResponse,
logApiError,
} from '../telemetry/index.js';
import {
ContentGenerator,
createContentGenerator,
@@ -223,80 +218,6 @@ export class GeminiClient {
return turn;
}
private _logApiRequest(model: string, inputTokenCount: number): void {
logApiRequest(this.config, {
model,
input_token_count: inputTokenCount,
duration_ms: 0, // Duration is not known at request time
});
}
private _logApiResponse(
model: string,
durationMs: number,
attempt: number,
response: GenerateContentResponse,
): void {
const promptFeedback = response.promptFeedback;
const finishReason = response.candidates?.[0]?.finishReason;
let responseError;
if (promptFeedback?.blockReason) {
responseError = `Blocked: ${promptFeedback.blockReason}${promptFeedback.blockReasonMessage ? ' - ' + promptFeedback.blockReasonMessage : ''}`;
} else if (
finishReason &&
!['STOP', 'MAX_TOKENS', 'UNSPECIFIED'].includes(finishReason)
) {
responseError = `Finished with reason: ${finishReason}`;
}
logApiResponse(this.config, {
model,
duration_ms: durationMs,
attempt,
status_code: undefined,
error: responseError,
output_token_count: response.usageMetadata?.candidatesTokenCount ?? 0,
cached_content_token_count:
response.usageMetadata?.cachedContentTokenCount ?? 0,
thoughts_token_count: response.usageMetadata?.thoughtsTokenCount ?? 0,
tool_token_count: response.usageMetadata?.toolUsePromptTokenCount ?? 0,
response_text: getResponseText(response),
});
}
private _logApiError(
model: string,
error: unknown,
durationMs: number,
attempt: number,
isAbort: boolean = false,
): void {
let statusCode: number | string | undefined;
let errorMessage = getErrorMessage(error);
if (isAbort) {
errorMessage = 'Request aborted by user';
statusCode = 'ABORTED'; // Custom S
} else if (typeof error === 'object' && error !== null) {
if ('status' in error) {
statusCode = (error as { status: number | string }).status;
} else if ('code' in error) {
statusCode = (error as { code: number | string }).code;
} else if ('httpStatusCode' in error) {
statusCode = (error as { httpStatusCode: number | string })
.httpStatusCode;
}
}
logApiError(this.config, {
model,
error: errorMessage,
status_code: statusCode,
duration_ms: durationMs,
attempt,
});
}
async generateJson(
contents: Content[],
schema: SchemaUnion,
@@ -305,8 +226,6 @@ export class GeminiClient {
config: GenerateContentConfig = {},
): Promise<Record<string, unknown>> {
const cg = await this.contentGenerator;
const attempt = 1;
const startTime = Date.now();
try {
const userMemory = this.config.getUserMemory();
const systemInstruction = getCoreSystemPrompt(userMemory);
@@ -316,22 +235,6 @@ export class GeminiClient {
...config,
};
let inputTokenCount = 0;
try {
const { totalTokens } = await cg.countTokens({
model,
contents,
});
inputTokenCount = totalTokens || 0;
} catch (_e) {
console.warn(
`Failed to count tokens for model ${model}. Proceeding with inputTokenCount = 0. Error: ${getErrorMessage(_e)}`,
);
inputTokenCount = 0;
}
this._logApiRequest(model, inputTokenCount);
const apiCall = () =>
cg.generateContent({
model,
@@ -345,7 +248,6 @@ export class GeminiClient {
});
const result = await retryWithBackoff(apiCall);
const durationMs = Date.now() - startTime;
const text = getResponseText(result);
if (!text) {
@@ -358,12 +260,10 @@ export class GeminiClient {
contents,
'generateJson-empty-response',
);
this._logApiError(model, error, durationMs, attempt);
throw error;
}
try {
const parsedJson = JSON.parse(text);
this._logApiResponse(model, durationMs, attempt, result);
return parsedJson;
} catch (parseError) {
await reportError(
@@ -375,15 +275,12 @@ export class GeminiClient {
},
'generateJson-parse',
);
this._logApiError(model, parseError, durationMs, attempt);
throw new Error(
`Failed to parse API response as JSON: ${getErrorMessage(parseError)}`,
);
}
} catch (error) {
const durationMs = Date.now() - startTime;
if (abortSignal.aborted) {
this._logApiError(model, error, durationMs, attempt, true);
throw error;
}
@@ -394,7 +291,6 @@ export class GeminiClient {
) {
throw error;
}
this._logApiError(model, error, durationMs, attempt);
await reportError(
error,
@@ -419,8 +315,6 @@ export class GeminiClient {
...this.generateContentConfig,
...generationConfig,
};
const attempt = 1;
const startTime = Date.now();
try {
const userMemory = this.config.getUserMemory();
@@ -432,22 +326,6 @@ export class GeminiClient {
systemInstruction,
};
let inputTokenCount = 0;
try {
const { totalTokens } = await cg.countTokens({
model: modelToUse,
contents,
});
inputTokenCount = totalTokens || 0;
} catch (_e) {
console.warn(
`Failed to count tokens for model ${modelToUse}. Proceeding with inputTokenCount = 0. Error: ${getErrorMessage(_e)}`,
);
inputTokenCount = 0;
}
this._logApiRequest(modelToUse, inputTokenCount);
const apiCall = () =>
cg.generateContent({
model: modelToUse,
@@ -460,18 +338,12 @@ export class GeminiClient {
'Raw API Response in client.ts:',
JSON.stringify(result, null, 2),
);
const durationMs = Date.now() - startTime;
this._logApiResponse(modelToUse, durationMs, attempt, result);
return result;
} catch (error: unknown) {
const durationMs = Date.now() - startTime;
if (abortSignal.aborted) {
this._logApiError(modelToUse, error, durationMs, attempt, true);
throw error;
}
this._logApiError(modelToUse, error, durationMs, attempt);
await reportError(
error,
`Error generating content via API with model ${modelToUse}.`,