MCP Terminal Server

/** * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { ValueType } from '@opentelemetry/api'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { createHash } from 'crypto'; import { GenerateRequestData, GenerateResponseData, GenerationUsage, GENKIT_VERSION, MediaPart, Part, ToolRequestPart, ToolResponsePart, } from 'genkit'; import { logger } from 'genkit/logging'; import { PathMetadata, toDisplayPath } from 'genkit/tracing'; import { internalMetricNamespaceWrap, MetricCounter, MetricHistogram, Telemetry, } from '../metrics.js'; import { createCommonLogAttributes, extractErrorName, extractOuterFeatureNameFromPath, truncate, truncatePath, } from '../utils.js'; type SharedDimensions = { modelName?: string; featureName?: string; path?: string; temperature?: number; topK?: number; topP?: number; status?: string; source?: string; sourceVersion?: string; }; class GenerateTelemetry implements Telemetry { /** * Wraps the declared metrics in a Genkit-specific, internal namespace. */ private _N = internalMetricNamespaceWrap.bind(null, 'ai'); private actionCounter = new MetricCounter(this._N('generate/requests'), { description: 'Counts calls to genkit generate actions.', valueType: ValueType.INT, }); private latencies = new MetricHistogram(this._N('generate/latency'), { description: 'Latencies when interacting with a Genkit model.', valueType: ValueType.DOUBLE, unit: 'ms', }); private inputCharacters = new MetricCounter( this._N('generate/input/characters'), { description: 'Counts input characters to any Genkit model.', valueType: ValueType.INT, } ); private inputTokens = new MetricCounter(this._N('generate/input/tokens'), { description: 'Counts input tokens to a Genkit model.', valueType: ValueType.INT, }); private inputImages = new MetricCounter(this._N('generate/input/images'), { description: 'Counts input images to a Genkit model.', valueType: ValueType.INT, }); private inputVideos = new MetricCounter(this._N('generate/input/videos'), { description: 'Counts input videos to a Genkit model.', valueType: ValueType.INT, }); private inputAudio = new MetricCounter(this._N('generate/input/audio'), { description: 'Counts input audio files to a Genkit model.', valueType: ValueType.INT, }); private outputCharacters = new MetricCounter( this._N('generate/output/characters'), { description: 'Counts output characters from a Genkit model.', valueType: ValueType.INT, } ); private outputTokens = new MetricCounter(this._N('generate/output/tokens'), { description: 'Counts output tokens from a Genkit model.', valueType: ValueType.INT, }); private outputImages = new MetricCounter(this._N('generate/output/images'), { description: 'Count output images from a Genkit model.', valueType: ValueType.INT, }); private outputVideos = new MetricCounter(this._N('generate/output/videos'), { description: 'Count output videos from a Genkit model.', valueType: ValueType.INT, }); private outputAudio = new MetricCounter(this._N('generate/output/audio'), { description: 'Count output audio files from a Genkit model.', valueType: ValueType.INT, }); tick( span: ReadableSpan, paths: Set<PathMetadata>, logInputAndOutput: boolean, projectId?: string ): void { const attributes = span.attributes; const modelName = truncate(attributes['genkit:name'] as string, 1024); const path = (attributes['genkit:path'] as string) || ''; const input = 'genkit:input' in attributes ? (JSON.parse( attributes['genkit:input']! as string ) as GenerateRequestData) : undefined; const output = 'genkit:output' in attributes ? (JSON.parse( attributes['genkit:output']! as string ) as GenerateResponseData) : undefined; const errName = extractErrorName(span.events); let featureName = truncate( (attributes['genkit:metadata:flow:name'] || extractOuterFeatureNameFromPath(path)) as string ); if (!featureName || featureName === '<unknown>') { featureName = 'generate'; } const sessionId = attributes['genkit:sessionId'] as string; const threadName = attributes['genkit:threadName'] as string; if (input) { this.recordGenerateActionMetrics(modelName, featureName, path, { response: output, errName, }); this.recordGenerateActionConfigLogs( span, modelName, featureName, path, input, projectId, sessionId, threadName ); if (logInputAndOutput) { this.recordGenerateActionInputLogs( span, modelName, featureName, path, input, projectId, sessionId, threadName ); } } if (output && logInputAndOutput) { this.recordGenerateActionOutputLogs( span, modelName, featureName, path, output, projectId, sessionId, threadName ); } } private recordGenerateActionMetrics( modelName: string, featureName: string, path: string, opts: { response?: GenerateResponseData; errName?: string; } ) { this.doRecordGenerateActionMetrics(modelName, opts.response?.usage || {}, { featureName, path, latencyMs: opts.response?.latencyMs, errName: opts.errName, source: 'ts', sourceVersion: GENKIT_VERSION, }); } private recordGenerateActionConfigLogs( span: ReadableSpan, model: string, featureName: string, qualifiedPath: string, input: GenerateRequestData, projectId?: string, sessionId?: string, threadName?: string ) { const path = truncatePath(toDisplayPath(qualifiedPath)); const sharedMetadata = { ...createCommonLogAttributes(span, projectId), model, path, qualifiedPath, featureName, sessionId, threadName, }; logger.logStructured(`Config[${path}, ${model}]`, { ...sharedMetadata, temperature: input.config?.temperature, topK: input.config?.topK, topP: input.config?.topP, maxOutputTokens: input.config?.maxOutputTokens, stopSequences: truncate(input.config?.stopSequences, 1024), source: 'ts', sourceVersion: GENKIT_VERSION, }); } private recordGenerateActionInputLogs( span: ReadableSpan, model: string, featureName: string, qualifiedPath: string, input: GenerateRequestData, projectId?: string, sessionId?: string, threadName?: string ) { const path = truncatePath(toDisplayPath(qualifiedPath)); const sharedMetadata = { ...createCommonLogAttributes(span, projectId), model, path, qualifiedPath, featureName, sessionId, threadName, }; const messages = input.messages.length; input.messages.forEach((msg, msgIdx) => { const parts = msg.content.length; msg.content.forEach((part, partIdx) => { const partCounts = this.toPartCounts(partIdx, parts, msgIdx, messages); logger.logStructured(`Input[${path}, ${model}] ${partCounts}`, { ...sharedMetadata, content: this.toPartLogContent(part), partIndex: partIdx, totalParts: parts, messageIndex: msgIdx, totalMessages: messages, }); }); }); } private recordGenerateActionOutputLogs( span: ReadableSpan, model: string, featureName: string, qualifiedPath: string, output: GenerateResponseData, projectId?: string, sessionId?: string, threadName?: string ) { const path = truncatePath(toDisplayPath(qualifiedPath)); const sharedMetadata = { ...createCommonLogAttributes(span, projectId), model, path, qualifiedPath, featureName, sessionId, threadName, }; const message = output.message || output.candidates?.[0]?.message!; const parts = message.content.length; message.content.forEach((part, partIdx) => { const partCounts = this.toPartCounts(partIdx, parts, 0, 1); const initial = output.finishMessage ? { finishMessage: truncate(output.finishMessage) } : {}; logger.logStructured(`Output[${path}, ${model}] ${partCounts}`, { ...initial, ...sharedMetadata, content: this.toPartLogContent(part), partIndex: partIdx, totalParts: parts, candidateIndex: 0, totalCandidates: 1, messageIndex: 0, finishReason: output.finishReason, }); }); } private toPartCounts( partOrdinal: number, parts: number, msgOrdinal: number, messages: number ): string { if (parts > 1 && messages > 1) { return `(part ${this.xOfY(partOrdinal, parts)} in message ${this.xOfY( msgOrdinal, messages )})`; } if (parts > 1) { return `(part ${this.xOfY(partOrdinal, parts)})`; } if (messages > 1) { return `(message ${this.xOfY(msgOrdinal, messages)})`; } return ''; } private xOfY(x: number, y: number): string { return `${x + 1} of ${y}`; } private toPartLogContent(part: Part): string { if (part.text) { return truncate(part.text); } if (part.data) { return truncate(JSON.stringify(part.data)); } if (part.media) { return this.toPartLogMedia(part); } if (part.toolRequest) { return this.toPartLogToolRequest(part); } if (part.toolResponse) { return this.toPartLogToolResponse(part); } if (part.custom) { return truncate(JSON.stringify(part.custom)); } return '<unknown format>'; } private toPartLogMedia(part: MediaPart): string { if (part.media.url.startsWith('data:')) { const splitIdx = part.media.url.indexOf('base64,'); if (splitIdx < 0) { return '<unknown media format>'; } const prefix = part.media.url.substring(0, splitIdx + 7); const hashedContent = createHash('sha256') .update(part.media.url.substring(splitIdx + 7)) .digest('hex'); return `${prefix}<sha256(${hashedContent})>`; } return truncate(part.media.url); } private toPartLogToolRequest(part: ToolRequestPart): string { const inputText = typeof part.toolRequest.input === 'string' ? part.toolRequest.input : JSON.stringify(part.toolRequest.input); return truncate( `Tool request: ${part.toolRequest.name}, ref: ${part.toolRequest.ref}, input: ${inputText}` ); } private toPartLogToolResponse(part: ToolResponsePart): string { const outputText = typeof part.toolResponse.output === 'string' ? part.toolResponse.output : JSON.stringify(part.toolResponse.output); return truncate( `Tool response: ${part.toolResponse.name}, ref: ${part.toolResponse.ref}, output: ${outputText}` ); } /** * Records all metrics associated with performing a GenerateAction. */ private doRecordGenerateActionMetrics( modelName: string, usage: GenerationUsage, dimensions: { featureName?: string; path?: string; latencyMs?: number; errName?: string; source?: string; sourceVersion: string; } ) { const shared: SharedDimensions = { modelName: modelName, featureName: dimensions.featureName, path: dimensions.path, source: dimensions.source, sourceVersion: dimensions.sourceVersion, status: dimensions.errName ? 'failure' : 'success', }; this.actionCounter.add(1, { error: dimensions.errName, ...shared, }); this.latencies.record(dimensions.latencyMs, shared); // inputs this.inputTokens.add(usage.inputTokens, shared); this.inputCharacters.add(usage.inputCharacters, shared); this.inputImages.add(usage.inputImages, shared); this.inputVideos.add(usage.inputVideos, shared); this.inputAudio.add(usage.inputAudioFiles, shared); // outputs this.outputTokens.add(usage.outputTokens, shared); this.outputCharacters.add(usage.outputCharacters, shared); this.outputImages.add(usage.outputImages, shared); this.outputVideos.add(usage.outputVideos, shared); this.outputAudio.add(usage.outputAudioFiles, shared); } } const generateTelemetry = new GenerateTelemetry(); export { generateTelemetry };