Escribe un complemento de telemetría de Genkit

OpenTelemetry admite la recopilación de seguimientos, métricas y registros. Firebase Genkit se puede extender para exportar todos los datos de telemetría a cualquier sistema compatible con OpenTelemetry escribiendo un complemento de telemetría que configure el SDK de Node.js.

Configuración

Para controlar la exportación de telemetría, el PluginOptions de tu complemento debe proporcionar un objeto telemetry que se ajuste al bloque telemetry de la configuración de Genkit.

export interface InitializedPlugin {
  ...
  telemetry?: {
    instrumentation?: Provider<TelemetryConfig>;
    logger?: Provider<LoggerConfig>;
  };
}

Este objeto puede proporcionar dos configuraciones separadas:

  • instrumentation: Proporciona la configuración de OpenTelemetry para Traces y Metrics.
  • logger: Proporciona el registrador subyacente que usa Genkit para escribir datos de registro estructurados, incluidas las entradas y salidas de los flujos de Genkit.

Esta separación es necesaria en la actualidad porque la funcionalidad de registro para el SDK de OpenTelemetry de Node.js aún está en desarrollo. El registro se proporciona por separado para que un complemento pueda controlar dónde se escriben los datos de forma explícita.

import { genkitPlugin, Plugin } from '@genkit-ai/core';

...

export interface MyPluginOptions {
  // [Optional] Your plugin options
}

export const myPlugin: Plugin<[MyPluginOptions] | []> = genkitPlugin(
  'myPlugin',
  async (options?: MyPluginOptions) => {
    return {
      telemetry: {
        instrumentation: {
          id: 'myPlugin',
          value: myTelemetryConfig,
        },
        logger: {
          id: 'myPlugin',
          value: myLogger,
        },
      },
    };
  }
);

export default myPlugin;

Con el bloque de código anterior, tu complemento ahora le proporcionará a Genkit una colaboración de telemetría que los desarrolladores pueden usar.

Instrumentación

Para controlar la exportación de seguimientos y métricas, tu complemento debe proporcionar una propiedad instrumentation en el objeto telemetry que cumpla con la interfaz TelemetryConfig:

interface TelemetryConfig {
  getConfig(): Partial<NodeSDKConfiguration>;
}

Esto proporciona una Partial<NodeSDKConfiguration> que el framework de Genkit utilizará para iniciar la NodeSDK. Esto le brinda al complemento control completo de cómo Genkit usa la integración de OpenTelemetry.

Por ejemplo, la siguiente configuración de telemetría proporciona un exportador simple de métricas y registros en la memoria:

import { AggregationTemporality, InMemoryMetricExporter, MetricReader, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { AlwaysOnSampler, BatchSpanProcessor, InMemorySpanExporter } from '@opentelemetry/sdk-trace-base';
import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
import { Resource } from '@opentelemetry/resources';
import { TelemetryConfig } from '@genkit-ai/core';

...

const myTelemetryConfig: TelemetryConfig = {
  getConfig(): Partial<NodeSDKConfiguration> {
    return {
      resource: new Resource({}),
      spanProcessor: new BatchSpanProcessor(new InMemorySpanExporter()),
      sampler: new AlwaysOnSampler(),
      instrumentations: myPluginInstrumentations,
      metricReader: new PeriodicExportingMetricReader({
        exporter: new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE),
      }),
    };
  },
};

Logger

Para controlar el registrador que usa el framework de Genkit para escribir datos de registro estructurados, el complemento debe proporcionar una propiedad logger en el objeto telemetry que se ajuste a la interfaz LoggerConfig:

interface LoggerConfig {
  getLogger(env: string): any;
}
{
  debug(...args: any);
  info(...args: any);
  warn(...args: any);
  error(...args: any);
  level: string;
}

Los frameworks de registro más populares se ajustan a esto. Uno de esos marcos de trabajo es winston, que permite configurar transportadores que pueden enviar directamente los datos de registro a la ubicación que elijas.

Por ejemplo, para proporcionar un registrador de winston que escriba datos de registro en la consola, puedes actualizar el registrador de complementos para que use lo siguiente:

import * as winston from 'winston';

...

const myLogger: LoggerConfig = {
  getLogger(env: string) {
    return winston.createLogger({
      transports: [new winston.transports.Console()],
      format: winston.format.printf((info): string => {
        return `[${info.level}] ${info.message}`;
      }),
    });
  }
};

Vincular registros y seguimientos

A menudo, es conveniente que tus instrucciones de registro se correlacionen con los seguimientos de OpenTelemetry exportados por el complemento. Debido a que el framework de OpenTelemetry no exporta las instrucciones de registro directamente, esto no ocurre de inmediato. Afortunadamente, OpenTelemetry admite instrumentaciones que copiarán los IDs de seguimiento y intervalo en las instrucciones de registro para frameworks de registro populares, como winston y pino. Si usas el paquete @opentelemetry/auto-instrumentations-node, puedes configurar estas y otras instrumentaciones (y otras) automáticamente. Sin embargo, en algunos casos, es posible que debas controlar los nombres y valores de los campos para los seguimientos y los intervalos. Para ello, tendrás que proporcionar una instrumentación de LogHook personalizada a la configuración de NodeSDK que proporciona tu TelemetryConfig:

import { Instrumentation } from '@opentelemetry/instrumentation';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
import { Span } from '@opentelemetry/api';

const myPluginInstrumentations: Instrumentation[] =
  getNodeAutoInstrumentations().concat([
    new WinstonInstrumentation({
      logHook: (span: Span, record: any) => {
        record['my-trace-id'] = span.spanContext().traceId;
        record['my-span-id'] = span.spanContext().spanId;
        record['is-trace-sampled'] = span.spanContext().traceFlags;
      },
    }),
  ]);

En el ejemplo, se habilitan todas las instrumentaciones automáticas para el NodeSDK de OpenTelemetry y, luego, se proporciona un WinstonInstrumentation personalizado que escribe los IDs de seguimiento y de intervalo en campos personalizados del mensaje de registro.

El framework de Genkit garantizará que el TelemetryConfig de tu complemento se inicializará antes que el LoggerConfig de tu complemento, pero debes asegurarte de que el registrador subyacente no se importe hasta que se inicialice LoggerConfig. Por ejemplo, el loggingConfig anterior se puede modificar de la siguiente manera:

const myLogger: LoggerConfig = {
  async getLogger(env: string) {
    // Do not import winston before calling getLogger so that the NodeSDK
    // instrumentations can be registered first.
    const winston = await import('winston');

    return winston.createLogger({
      transports: [new winston.transports.Console()],
      format: winston.format.printf((info): string => {
        return `[${info.level}] ${info.message}`;
      }),
    });
  },
};

Ejemplo completo

El siguiente es un ejemplo completo del complemento de telemetría creado anteriormente. Para ver un ejemplo real, observa el complemento @genkit-ai/google-cloud.

import {
  genkitPlugin,
  LoggerConfig,
  Plugin,
  TelemetryConfig,
} from '@genkit-ai/core';
import { Span } from '@opentelemetry/api';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Instrumentation } from '@opentelemetry/instrumentation';
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
import { Resource } from '@opentelemetry/resources';
import {
  AggregationTemporality,
  InMemoryMetricExporter,
  PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
import {
  AlwaysOnSampler,
  BatchSpanProcessor,
  InMemorySpanExporter,
} from '@opentelemetry/sdk-trace-base';

export interface MyPluginOptions {
  // [Optional] Your plugin options
}

const myPluginInstrumentations: Instrumentation[] =
  getNodeAutoInstrumentations().concat([
    new WinstonInstrumentation({
      logHook: (span: Span, record: any) => {
        record['my-trace-id'] = span.spanContext().traceId;
        record['my-span-id'] = span.spanContext().spanId;
        record['is-trace-sampled'] = span.spanContext().traceFlags;
      },
    }),
  ]);

const myTelemetryConfig: TelemetryConfig = {
  getConfig(): Partial<NodeSDKConfiguration> {
    return {
      resource: new Resource({}),
      spanProcessor: new BatchSpanProcessor(new InMemorySpanExporter()),
      sampler: new AlwaysOnSampler(),
      instrumentations: myPluginInstrumentations,
      metricReader: new PeriodicExportingMetricReader({
        exporter: new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE),
      }),
    };
  },
};

const myLogger: LoggerConfig = {
  async getLogger(env: string) {
    // Do not import winston before calling getLogger so that the NodeSDK
    // instrumentations can be registered first.
    const winston = await import('winston');

    return winston.createLogger({
      transports: [new winston.transports.Console()],
      format: winston.format.printf((info): string => {
        return `[${info.level}] ${info.message}`;
      }),
    });
  },
};

export const myPlugin: Plugin<[MyPluginOptions] | []> = genkitPlugin(
  'myPlugin',
  async (options?: MyPluginOptions) => {
    return {
      telemetry: {
        instrumentation: {
          id: 'myPlugin',
          value: myTelemetryConfig,
        },
        logger: {
          id: 'myPlugin',
          value: myLogger,
        },
      },
    };
  }
);

export default myPlugin;

Soluciona problemas

Si tienes problemas para que los datos se muestren donde esperas, OpenTelemetry proporciona una herramienta de diagnóstico útil que ayuda a encontrar la fuente del problema.