Scrivere un plug-in di telemetria Genkit

OpenTelemetry supporta la raccolta di tracce, metriche e log. Firebase Genkit può essere esteso per esportare tutti i dati di telemetria in qualsiasi sistema che supporta OpenTelemetry scrivendo un plug-in di telemetria che configuri Node.js l'SDK.

Configurazione

Per controllare l'esportazione della telemetria, il parametro PluginOptions del plug-in deve fornire un Oggetto telemetry conforme al blocco telemetry nella configurazione di Genkit.

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

Questo oggetto può fornire due configurazioni distinte:

  • instrumentation: fornisce la configurazione di OpenTelemetry per Traces e Metrics.
  • logger: fornisce il logger di base utilizzato da Genkit per scrivere dati dei log strutturati, inclusi input e output dei flussi Genkit.

Questa separazione è attualmente necessaria perché la funzionalità di logging per Node.js L'SDK OpenTelemetry è ancora in fase di sviluppo. Il logging è fornito separatamente in modo che un plug-in possa controllare la posizione dei dati scritte in modo esplicito.

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 il blocco di codice riportato sopra, il plug-in fornirà a Genkit una configurazione della telemetria che può essere utilizzata dagli sviluppatori.

Strumentazione

Per controllare l'esportazione di tracce e metriche, il plug-in deve fornire una proprietà instrumentation nell'oggetto telemetry conforme all'interfaccia TelemetryConfig:

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

In questo modo viene fornito un valore Partial<NodeSDKConfiguration> che verrà utilizzato il framework Genkit per avviare NodeSDK In questo modo, il plug-in ha il controllo completo sull'utilizzo dell'integrazione di OpenTelemetry da parte di Genkit.

Ad esempio, la seguente configurazione di telemetria fornisce una semplice traccia in memoria ed esportatore di metriche:

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

Per controllare il logger utilizzato dal framework Genkit per scrivere dati di log strutturati, il plug-in deve fornire una proprietà logger sull'oggetto telemetry conforme alle Interfaccia LoggerConfig:

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

La maggior parte dei framework di logging è conforme a questo standard. Uno di questi framework è winston, che consente di configurare i trasportatori che possono inviare direttamente i dati dei log in una posizione a tua scelta.

Ad esempio, per fornire un logger Winston che scrive i dati di log nella console, puoi aggiornare il tuo logger dei plug-in in modo da utilizzare quanto segue:

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

Collegamento di log e tracce

Spesso è opportuno correlare le istruzioni di log con le tracce OpenTelemetry esportate dal plug-in. Poiché le istruzioni di log non sono direttamente dal framework OpenTelemetry, ciò non avviene . Fortunatamente, OpenTelemetry supporta le misurazioni che copiano gli ID traccia e span nelle istruzioni di log per i framework di logging più diffusi come winston e pino. Utilizzando il pacchetto @opentelemetry/auto-instrumentations-node, puoi configurare automaticamente queste e altre misurazioni, ma in alcuni casi potresti dover controllare i nomi e i valori dei campi per le tracce e gli span. Per farlo, dovrai fornire un'instrumentazione LogHook personalizzata alla configurazione NodeSDK fornita dal tuo 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'esempio attiva tutte le ispezioni automatiche per il NodeSDK OpenTelemetry, quindi fornisce un WinstonInstrumentation personalizzato che scrive gli ID traccia e WinstonInstrumentation nei campi personalizzati del messaggio di log.

Il framework Genkit garantirà che il valore TelemetryConfig del plug-in venga inizializzato prima del plug-in LoggerConfig, ma devi prestare attenzione assicura che il logger sottostante non venga importato finché il valore inizializzato. Ad esempio, il loggingConfig sopra può essere modificato come segue:

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

Esempio completo

Di seguito è riportato un esempio completo del plug-in di telemetria creato sopra. Per un esempio reale, dai un'occhiata al 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;

Risoluzione dei problemi

Se hai difficoltà a visualizzare i dati dove ti aspetti, OpenTelemetry fornisce un utile strumento di diagnostica che aiuta a individuare l'origine del problema.