Écrire un plug-in de télémétrie Genkit

OpenTelemetry permet de collecter des traces, des métriques et des journaux. Firebase Genkit peut être étendu pour exporter toutes les données de télémétrie vers n'importe quel système compatible avec OpenTelemetry en écrivant un plug-in de télémétrie qui configure le Node.js SDK.

Configuration

Pour contrôler l'exportation de télémétrie, le fichier PluginOptions de votre plug-in doit fournir un Objet telemetry conforme au bloc telemetry de la configuration de Genkit.

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

Cet objet peut fournir deux configurations distinctes:

  • instrumentation: fournit la configuration OpenTelemetry pour Traces et Metrics
  • logger: fournit l'enregistreur sous-jacent utilisé par Genkit pour écrire de journaux structurées, y compris les entrées et les sorties des flux Genkit.

Cette séparation est actuellement nécessaire, car la fonctionnalité de journalisation pour Node.js Le SDK OpenTelemetry est encore en cours de développement. Logging est fourni séparément afin qu'un plug-in puisse contrôler l'emplacement des données n'a pas été rédigée explicitement.

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;

Avec le bloc de code ci-dessus, votre plug-in fournira désormais à Genkit une configuration de télémétrie pouvant être utilisée par les développeurs.

Instrumentation

Pour contrôler l'exportation des traces et des métriques, votre plug-in doit fournir un Propriété instrumentation de l'objet telemetry conforme à la Interface TelemetryConfig:

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

Cela fournit un Partial<NodeSDKConfiguration> qui sera utilisé par le framework Genkit pour démarrer NodeSDK. Le plug-in peut ainsi contrôler totalement l'utilisation de l'intégration d'OpenTelemetry. par Genkit.

Par exemple, la configuration de télémétrie suivante fournit un exportateur de trace et de métriques en mémoire simple :

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

Pour contrôler le journal utilisé par le framework Genkit pour écrire des données de journal structurées, le plug-in doit fournir une propriété logger sur l'objet telemetry conforme à l'interface LoggerConfig :

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

La plupart des frameworks de journalisation les plus populaires sont conformes à cette règle. L'un de ces cadres est winston, qui permet de configurer qui peuvent transmettre directement les données du journal à l'emplacement de votre choix.

Par exemple, pour fournir un enregistreur Winston qui écrit les données de journal sur la console, vous pouvez mettre à jour votre enregistreur de plug-in pour utiliser ce qui suit:

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}`;
      }),
    });
  }
};

Associer des journaux et des traces

Il est souvent souhaitable d'établir une corrélation entre vos instructions de journalisation Traces OpenTelemetry exportées par votre plug-in. Étant donné que les instructions de journalisation ne sont pas directement exportées par le framework OpenTelemetry, cela ne se produit pas immédiatement. Heureusement, OpenTelemetry est compatible avec les instrumentations qui copient les ID de trace et de segment sur les instructions de journalisation pour les frameworks de journalisation populaires tels que winston et pino. En utilisant le package @opentelemetry/auto-instrumentations-node, vous pouvez configurer automatiquement ces instrumentations (et d'autres), mais dans Dans certains cas, vous devrez peut-être contrôler les noms et les valeurs des champs pour les traces et segments. Pour ce faire, vous devez fournir une instrumentation LogHook personnalisée la configuration NodeSDK fournie par votre 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;
      },
    }),
  ]);

L'exemple active toutes les instrumentations automatiques pour le NodeSDK OpenTelemetry, puis fournit un WinstonInstrumentation personnalisé qui écrit la trace et d'appliquer des ID à des champs personnalisés dans le message de journal.

Le framework Genkit garantit que le TelemetryConfig de votre plug-in sera initialisé avant le LoggerConfig de votre plug-in, mais vous devez vous assurer que le journaliseur sous-jacent n'est pas importé tant que le LoggerConfig n'est pas initialisé. Par exemple, l'objet loggingConfig ci-dessus peut être modifié comme suit:

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}`;
      }),
    });
  },
};

Exemple complet

Voici un exemple complet du plug-in de télémétrie créé ci-dessus. Pour un exemple concret, consultez le plug-in @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;

Dépannage

Si vous ne parvenez pas à faire apparaître les données là où vous le souhaitez, OpenTelemetry propose une fonctionnalité Outil de diagnostic qui permet de localiser la source du problème.