2025-10-27 19:16:44 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
diag,
|
|
|
|
|
SpanStatusCode,
|
|
|
|
|
trace,
|
|
|
|
|
type AttributeValue,
|
|
|
|
|
type SpanOptions,
|
|
|
|
|
} from '@opentelemetry/api';
|
|
|
|
|
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
2026-02-26 18:26:16 -08:00
|
|
|
import {
|
|
|
|
|
type GeminiCliOperation,
|
|
|
|
|
GEN_AI_AGENT_DESCRIPTION,
|
|
|
|
|
GEN_AI_AGENT_NAME,
|
|
|
|
|
GEN_AI_CONVERSATION_ID,
|
|
|
|
|
GEN_AI_INPUT_MESSAGES,
|
|
|
|
|
GEN_AI_OPERATION_NAME,
|
|
|
|
|
GEN_AI_OUTPUT_MESSAGES,
|
|
|
|
|
SERVICE_DESCRIPTION,
|
|
|
|
|
SERVICE_NAME,
|
|
|
|
|
} from './constants.js';
|
|
|
|
|
import { sessionId } from '../utils/session.js';
|
2025-10-27 19:16:44 -04:00
|
|
|
|
2026-03-23 19:58:06 -04:00
|
|
|
import { truncateString } from '../utils/textUtils.js';
|
|
|
|
|
|
2025-10-27 19:16:44 -04:00
|
|
|
const TRACER_NAME = 'gemini-cli';
|
|
|
|
|
const TRACER_VERSION = 'v1';
|
|
|
|
|
|
2026-03-23 19:58:06 -04:00
|
|
|
export function truncateForTelemetry(
|
|
|
|
|
value: unknown,
|
|
|
|
|
maxLength: number = 10000,
|
|
|
|
|
): AttributeValue | undefined {
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
return truncateString(
|
|
|
|
|
value,
|
|
|
|
|
maxLength,
|
|
|
|
|
`...[TRUNCATED: original length ${value.length}]`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === 'object' && value !== null) {
|
|
|
|
|
const stringified = safeJsonStringify(value);
|
|
|
|
|
return truncateString(
|
|
|
|
|
stringified,
|
|
|
|
|
maxLength,
|
|
|
|
|
`...[TRUNCATED: original length ${stringified.length}]`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isAsyncIterable<T>(value: T): value is T & AsyncIterable<unknown> {
|
|
|
|
|
return (
|
|
|
|
|
typeof value === 'object' && value !== null && Symbol.asyncIterator in value
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-27 19:16:44 -04:00
|
|
|
/**
|
|
|
|
|
* Metadata for a span.
|
|
|
|
|
*/
|
|
|
|
|
export interface SpanMetadata {
|
|
|
|
|
/** The name of the span. */
|
|
|
|
|
name: string;
|
|
|
|
|
/** The input to the span. */
|
|
|
|
|
input?: unknown;
|
|
|
|
|
/** The output of the span. */
|
|
|
|
|
output?: unknown;
|
|
|
|
|
error?: unknown;
|
|
|
|
|
/** Additional attributes for the span. */
|
|
|
|
|
attributes: Record<string, AttributeValue>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Runs a function in a new OpenTelemetry span.
|
|
|
|
|
*
|
|
|
|
|
* The `meta` object will be automatically used to set the span's status and attributes upon completion.
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```typescript
|
|
|
|
|
* runInDevTraceSpan({ name: 'my-operation' }, ({ metadata }) => {
|
|
|
|
|
* metadata.input = { foo: 'bar' };
|
|
|
|
|
* // ... do work ...
|
|
|
|
|
* metadata.output = { result: 'baz' };
|
|
|
|
|
* metadata.attributes['my.custom.attribute'] = 'some-value';
|
|
|
|
|
* });
|
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* @param opts The options for the span.
|
|
|
|
|
* @param fn The function to run in the span.
|
|
|
|
|
* @returns The result of the function.
|
|
|
|
|
*/
|
|
|
|
|
export async function runInDevTraceSpan<R>(
|
2026-03-23 19:58:06 -04:00
|
|
|
opts: SpanOptions & { operation: GeminiCliOperation; logPrompts?: boolean },
|
|
|
|
|
fn: ({ metadata }: { metadata: SpanMetadata }) => Promise<R>,
|
2025-10-27 19:16:44 -04:00
|
|
|
): Promise<R> {
|
2026-03-23 19:58:06 -04:00
|
|
|
const { operation, logPrompts, ...restOfSpanOpts } = opts;
|
2025-10-27 19:16:44 -04:00
|
|
|
|
|
|
|
|
const tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
2026-02-26 18:26:16 -08:00
|
|
|
return tracer.startActiveSpan(operation, restOfSpanOpts, async (span) => {
|
2025-12-02 07:11:40 +09:00
|
|
|
const meta: SpanMetadata = {
|
2026-02-26 18:26:16 -08:00
|
|
|
name: operation,
|
|
|
|
|
attributes: {
|
|
|
|
|
[GEN_AI_OPERATION_NAME]: operation,
|
|
|
|
|
[GEN_AI_AGENT_NAME]: SERVICE_NAME,
|
|
|
|
|
[GEN_AI_AGENT_DESCRIPTION]: SERVICE_DESCRIPTION,
|
|
|
|
|
[GEN_AI_CONVERSATION_ID]: sessionId,
|
|
|
|
|
},
|
2025-12-02 07:11:40 +09:00
|
|
|
};
|
|
|
|
|
const endSpan = () => {
|
|
|
|
|
try {
|
2026-03-23 19:58:06 -04:00
|
|
|
if (logPrompts !== false) {
|
|
|
|
|
if (meta.input !== undefined) {
|
|
|
|
|
const truncated = truncateForTelemetry(meta.input);
|
|
|
|
|
if (truncated !== undefined) {
|
|
|
|
|
span.setAttribute(GEN_AI_INPUT_MESSAGES, truncated);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (meta.output !== undefined) {
|
|
|
|
|
const truncated = truncateForTelemetry(meta.output);
|
|
|
|
|
if (truncated !== undefined) {
|
|
|
|
|
span.setAttribute(GEN_AI_OUTPUT_MESSAGES, truncated);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-02 07:11:40 +09:00
|
|
|
}
|
|
|
|
|
for (const [key, value] of Object.entries(meta.attributes)) {
|
2026-03-23 19:58:06 -04:00
|
|
|
const truncated = truncateForTelemetry(value);
|
|
|
|
|
if (truncated !== undefined) {
|
|
|
|
|
span.setAttribute(key, truncated);
|
|
|
|
|
}
|
2025-12-02 07:11:40 +09:00
|
|
|
}
|
|
|
|
|
if (meta.error) {
|
2025-10-27 19:16:44 -04:00
|
|
|
span.setStatus({
|
|
|
|
|
code: SpanStatusCode.ERROR,
|
2025-12-02 07:11:40 +09:00
|
|
|
message: getErrorMessage(meta.error),
|
2025-10-27 19:16:44 -04:00
|
|
|
});
|
2025-12-02 07:11:40 +09:00
|
|
|
if (meta.error instanceof Error) {
|
|
|
|
|
span.recordException(meta.error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
span.setStatus({ code: SpanStatusCode.OK });
|
2025-10-27 19:16:44 -04:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2025-12-02 07:11:40 +09:00
|
|
|
// Log the error but don't rethrow, to ensure span.end() is called.
|
|
|
|
|
diag.error('Error setting span attributes in endSpan', e);
|
|
|
|
|
span.setStatus({
|
|
|
|
|
code: SpanStatusCode.ERROR,
|
|
|
|
|
message: `Error in endSpan: ${getErrorMessage(e)}`,
|
|
|
|
|
});
|
2025-10-27 19:16:44 -04:00
|
|
|
} finally {
|
2025-12-02 07:11:40 +09:00
|
|
|
span.end();
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-23 19:58:06 -04:00
|
|
|
|
|
|
|
|
let isStream = false;
|
2025-12-02 07:11:40 +09:00
|
|
|
try {
|
2026-03-23 19:58:06 -04:00
|
|
|
const result = await fn({ metadata: meta });
|
|
|
|
|
|
|
|
|
|
if (isAsyncIterable(result)) {
|
|
|
|
|
isStream = true;
|
|
|
|
|
const streamWrapper = (async function* () {
|
|
|
|
|
try {
|
|
|
|
|
yield* result;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
meta.error = e;
|
|
|
|
|
throw e;
|
|
|
|
|
} finally {
|
|
|
|
|
endSpan();
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
return Object.assign(streamWrapper, result);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
2025-12-02 07:11:40 +09:00
|
|
|
} catch (e) {
|
|
|
|
|
meta.error = e;
|
|
|
|
|
throw e;
|
|
|
|
|
} finally {
|
2026-03-23 19:58:06 -04:00
|
|
|
if (!isStream) {
|
2025-12-02 07:11:40 +09:00
|
|
|
endSpan();
|
2025-10-27 19:16:44 -04:00
|
|
|
}
|
2025-12-02 07:11:40 +09:00
|
|
|
}
|
|
|
|
|
});
|
2025-10-27 19:16:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the error message from an error object.
|
|
|
|
|
*
|
|
|
|
|
* @param e The error object.
|
|
|
|
|
* @returns The error message.
|
|
|
|
|
*/
|
|
|
|
|
function getErrorMessage(e: unknown): string {
|
|
|
|
|
if (e instanceof Error) {
|
|
|
|
|
return e.message;
|
|
|
|
|
}
|
|
|
|
|
if (typeof e === 'string') {
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
return safeJsonStringify(e);
|
|
|
|
|
}
|