UNPKG

@genkit-ai/google-cloud

Version:

Genkit AI framework plugin for Google Cloud Platform including Firestore trace/state store and deployment helpers for Cloud Functions for Firebase.

364 lines (363 loc) 11.9 kB
import { MetricExporter } from "@google-cloud/opentelemetry-cloud-monitoring-exporter"; import { TraceExporter } from "@google-cloud/opentelemetry-cloud-trace-exporter"; import { GcpDetectorSync } from "@google-cloud/opentelemetry-resource-util"; import { SpanStatusCode, TraceFlags } from "@opentelemetry/api"; import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino"; import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston"; import { Resource } from "@opentelemetry/resources"; import { AggregationTemporality, DefaultAggregation, ExponentialHistogramAggregation, InMemoryMetricExporter, InstrumentType, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { BatchSpanProcessor, InMemorySpanExporter } from "@opentelemetry/sdk-trace-base"; import { GENKIT_VERSION } from "genkit"; import { logger } from "genkit/logging"; import { actionTelemetry } from "./telemetry/action.js"; import { engagementTelemetry } from "./telemetry/engagement.js"; import { featuresTelemetry } from "./telemetry/feature.js"; import { generateTelemetry } from "./telemetry/generate.js"; import { pathsTelemetry } from "./telemetry/path.js"; import { metricsDenied, metricsDeniedHelpText, tracingDenied, tracingDeniedHelpText } from "./utils.js"; let metricExporter; let spanProcessor; let spanExporter; class GcpOpenTelemetry { config; resource; constructor(config) { this.config = config; this.resource = new Resource({ type: "global" }).merge( new GcpDetectorSync().detect() ); } /** * Log hook for writing trace and span metadata to log messages in the format * required by GCP. */ gcpTraceLogHook = (span, record) => { const spanContext = span.spanContext(); const isSampled = !!(spanContext.traceFlags & TraceFlags.SAMPLED); const projectId = this.config.projectId; record["logging.googleapis.com/trace"] ??= `projects/${projectId}/traces/${spanContext.traceId}`; record["logging.googleapis.com/trace_sampled"] ??= isSampled ? "1" : "0"; record["logging.googleapis.com/spanId"] ??= spanContext.spanId; delete record["span_id"]; delete record["trace_id"]; delete record["trace_flags"]; }; async getConfig() { spanProcessor = new BatchSpanProcessor(await this.createSpanExporter()); return { resource: this.resource, spanProcessor, sampler: this.config.sampler, instrumentations: this.getInstrumentations(), metricReader: await this.createMetricReader() }; } async createSpanExporter() { spanExporter = new AdjustingTraceExporter( this.shouldExportTraces() ? new TraceExporter({ // provided projectId should take precedence over env vars, etc projectId: this.config.projectId, // creds for non-GCP environments, in lieu of using ADC. credentials: this.config.credentials }) : new InMemorySpanExporter(), this.config.exportInputAndOutput, this.config.projectId, getErrorHandler( (err) => { return tracingDenied(err); }, await tracingDeniedHelpText() ) ); return spanExporter; } /** * Creates a {MetricReader} for pushing metrics out to GCP via OpenTelemetry. */ async createMetricReader() { metricExporter = await this.buildMetricExporter(); return new PeriodicExportingMetricReader({ exportIntervalMillis: this.config.metricExportIntervalMillis, exportTimeoutMillis: this.config.metricExportTimeoutMillis, exporter: metricExporter }); } /** Gets all open telemetry instrumentations as configured by the plugin. */ getInstrumentations() { let instrumentations = []; if (this.config.autoInstrumentation) { instrumentations = getNodeAutoInstrumentations( this.config.autoInstrumentationConfig ); } return instrumentations.concat(this.getDefaultLoggingInstrumentations()).concat(this.config.instrumentations ?? []); } shouldExportTraces() { return this.config.export && !this.config.disableTraces; } shouldExportMetrics() { return this.config.export && !this.config.disableMetrics; } /** Always configure the Pino and Winston instrumentations */ getDefaultLoggingInstrumentations() { return [ new WinstonInstrumentation({ logHook: this.gcpTraceLogHook }), new PinoInstrumentation({ logHook: this.gcpTraceLogHook }) ]; } async buildMetricExporter() { const exporter = this.shouldExportMetrics() ? new MetricExporterWrapper( { userAgent: { product: "genkit", version: GENKIT_VERSION }, // provided projectId should take precedence over env vars, etc projectId: this.config.projectId, // creds for non-GCP environments, in lieu of using ADC. credentials: this.config.credentials }, getErrorHandler( (err) => { return metricsDenied(err); }, await metricsDeniedHelpText() ) ) : new InMemoryMetricExporter(AggregationTemporality.DELTA); return exporter; } } class MetricExporterWrapper extends MetricExporter { constructor(options, errorHandler) { super(options); this.errorHandler = errorHandler; } promise = new Promise((resolve) => resolve()); async export(metrics, resultCallback) { await this.promise; this.modifyStartTimes(metrics); this.promise = new Promise((resolve) => { super.export(metrics, (result) => { try { if (this.errorHandler && result.error) { this.errorHandler(result.error); } resultCallback(result); } finally { resolve(); } }); }); } selectAggregation(instrumentType) { if (instrumentType === InstrumentType.HISTOGRAM) { return new ExponentialHistogramAggregation(); } return new DefaultAggregation(); } selectAggregationTemporality(instrumentType) { return AggregationTemporality.DELTA; } /** * Modify the start times of each data point to ensure no * overlap with previous exports. * * Cloud metrics do not support delta metrics for custom metrics * and will convert any DELTA aggregations to CUMULATIVE ones on * export. There is implicit overlap in the start/end times that * the Metric reader is sending -- the end_time of the previous * export will become the start_time of the current export. The * overlap in times means that only one of those records will * persist and the other will be overwritten. This * method adds a thousandth of a second to ensure discrete export * timeframes. */ modifyStartTimes(metrics) { metrics.scopeMetrics.forEach((scopeMetric) => { scopeMetric.metrics.forEach((metric) => { metric.dataPoints.forEach((dataPoint) => { dataPoint.startTime[1] = dataPoint.startTime[1] + 1e6; }); }); }); } async shutdown() { return await this.forceFlush(); } async forceFlush() { await this.promise; } } class AdjustingTraceExporter { constructor(exporter, logInputAndOutput, projectId, errorHandler) { this.exporter = exporter; this.logInputAndOutput = logInputAndOutput; this.projectId = projectId; this.errorHandler = errorHandler; } export(spans, resultCallback) { this.exporter?.export(this.adjust(spans), (result) => { if (this.errorHandler && result.error) { this.errorHandler(result.error); } resultCallback(result); }); } shutdown() { return this.exporter?.shutdown(); } getExporter() { return this.exporter; } forceFlush() { if (this.exporter?.forceFlush) { return this.exporter.forceFlush(); } return Promise.resolve(); } adjust(spans) { return spans.map((span) => { this.tickTelemetry(span); span = this.redactInputOutput(span); span = this.markErrorSpanAsError(span); span = this.markFailedSpan(span); span = this.markGenkitFeature(span); span = this.markGenkitModel(span); span = this.normalizeLabels(span); return span; }); } tickTelemetry(span) { const attributes = span.attributes; if (!Object.keys(attributes).includes("genkit:type")) { return; } const type = attributes["genkit:type"]; const subtype = attributes["genkit:metadata:subtype"]; const isRoot = !!span.attributes["genkit:isRoot"]; pathsTelemetry.tick(span, this.logInputAndOutput, this.projectId); if (isRoot) { featuresTelemetry.tick(span, this.logInputAndOutput, this.projectId); span.attributes["genkit:rootState"] = span.attributes["genkit:state"]; } else { if (type === "action" && subtype === "model") { generateTelemetry.tick(span, this.logInputAndOutput, this.projectId); } if (type === "action" && subtype === "tool") { } if (type === "action" || type === "flow" || type == "flowStep" || type == "util") { actionTelemetry.tick(span, this.logInputAndOutput, this.projectId); } } if (type === "userEngagement") { engagementTelemetry.tick(span, this.logInputAndOutput, this.projectId); } } redactInputOutput(span) { const hasInput = "genkit:input" in span.attributes; const hasOutput = "genkit:output" in span.attributes; return !hasInput && !hasOutput ? span : { ...span, spanContext: span.spanContext, attributes: { ...span.attributes, "genkit:input": "<redacted>", "genkit:output": "<redacted>" } }; } // This is a workaround for GCP Trace to mark a span with a red // exclamation mark indicating that it is an error. markErrorSpanAsError(span) { return span.status.code !== SpanStatusCode.ERROR ? span : { ...span, spanContext: span.spanContext, attributes: { ...span.attributes, "/http/status_code": "599" } }; } normalizeLabels(span) { const normalized = {}; for (const [key, value] of Object.entries(span.attributes)) { normalized[key.replace(/\:/g, "/")] = value; } return { ...span, spanContext: span.spanContext, attributes: normalized }; } markFailedSpan(span) { if (span.attributes["genkit:isFailureSource"]) { span.attributes["genkit:failedSpan"] = span.attributes["genkit:name"]; span.attributes["genkit:failedPath"] = span.attributes["genkit:path"]; } return span; } markGenkitFeature(span) { if (span.attributes["genkit:isRoot"] && !!span.attributes["genkit:name"]) { span.attributes["genkit:feature"] = span.attributes["genkit:name"]; } return span; } markGenkitModel(span) { if (span.attributes["genkit:metadata:subtype"] === "model" && !!span.attributes["genkit:name"]) { span.attributes["genkit:model"] = span.attributes["genkit:name"]; } return span; } } function getErrorHandler(shouldLogFn, helpText) { let instructionsLogged = false; return (err) => { const defaultLogger = logger.defaultLogger; if (err && shouldLogFn(err)) { if (!instructionsLogged) { instructionsLogged = true; defaultLogger.error( `Unable to send telemetry to Google Cloud: ${err.message} ${helpText} ` ); } } else if (err) { defaultLogger.error(`Unable to send telemetry to Google Cloud: ${err}`); } }; } function __getMetricExporterForTesting() { return metricExporter; } function __getSpanExporterForTesting() { return spanExporter.getExporter(); } function __forceFlushSpansForTesting() { spanProcessor.forceFlush(); } export { GcpOpenTelemetry, __forceFlushSpansForTesting, __getMetricExporterForTesting, __getSpanExporterForTesting }; //# sourceMappingURL=gcpOpenTelemetry.mjs.map