Como criar um plug-in de telemetria do Genkit

O OpenTelemetry oferece suporte à coleta de traces, métricas e registros. O Firebase Genkit pode ser estendido para exportar todos os dados de telemetria para qualquer sistema compatível com o OpenTelemetry. Para isso, basta escrever um plug-in de telemetria que configure o Node.js SDK do Vertex AI Pipelines.

Configuração

Para controlar a exportação de telemetria, o PluginOptions do plug-in precisa fornecer uma Objeto telemetry que está em conformidade com o bloco telemetry na configuração do Genkit.

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

Esse objeto pode fornecer duas configurações separadas:

  • instrumentation: fornece a configuração do OpenTelemetry para Traces e Metrics
  • logger: fornece o logger usado pelo Genkit para gravação dados de registro estruturados, incluindo entradas e saídas de fluxos do Genkit.

No momento, essa separação é necessária porque a funcionalidade de geração de registros para Node.js O SDK do OpenTelemetry ainda está em desenvolvimento. A geração de registros é fornecida separadamente para que um plug-in possa controlar onde os dados são escritos explicitamente.

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;

Com o bloco de código acima, seu plug-in vai fornecer ao Genkit uma telemetria configuração que pode ser usada por desenvolvedores.

Instrumentação

Para controlar a exportação de traces e métricas, seu plug-in precisa fornecer um propriedade instrumentation no objeto telemetry que esteja em conformidade com a Interface TelemetryConfig:

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

Isso fornece um Partial<NodeSDKConfiguration> que será usado pelo Genkit para iniciar o NodeSDK. Isso dá ao plug-in controle completo de como a integração do OpenTelemetry é usada. pelo Genkit.

Por exemplo, a configuração de telemetria a seguir fornece um exportador de métricas e traces na memória simples:

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 o logger usado pelo framework do Genkit para gravar dados de registro estruturados, o plug-in precisa fornecer uma propriedade logger no objeto telemetry que esteja em conformidade com a Interface LoggerConfig:

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

As estruturas de geração de registros mais conhecidas estão em conformidade com isso. Uma dessas estruturas é winston, que permite configurar transportadores que podem enviar diretamente os dados de registro para um local de sua escolha.

Por exemplo, para fornecer um logger Winston que grava dados de registro no console, você pode atualizar o registrador de plug-ins para usar o seguinte:

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

Como vincular registros e traces

Muitas vezes, é desejável ter os log statements correlacionados com o Traces do OpenTelemetry exportados pelo plug-in. Como os log statements não são exportados diretamente pelo framework do OpenTelemetry, isso não acontece da caixa Felizmente, o OpenTelemetry oferece suporte a instrumentações que copiam rastros e IDs de período em log statements para frameworks conhecidos de geração de registros, como winston e pino. Ao usar o pacote @opentelemetry/auto-instrumentations-node, é possível ter essas (e outras) instrumentações configuradas automaticamente, mas em alguns casos, pode ser necessário controlar os nomes e valores dos campos para traces e vãos. Para isso, forneça uma instrumentação do LogHook personalizada para a configuração do SDK do Node fornecida pelo 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;
      },
    }),
  ]);

O exemplo ativa todas as instrumentações automáticas para NodeSDK do OpenTelemetry. e, em seguida, fornece uma WinstonInstrumentation personalizada que grava o trace e de IDs de período a campos personalizados na mensagem de registro.

O framework do Genkit garante que o TelemetryConfig do plug-in será inicializado antes do LoggerConfig do plug-in, mas é preciso tomar cuidado para garantir que o logger subjacente não seja importado até que o LoggerConfig seja inicializado. Por exemplo, o loggingConfig acima pode ser modificado da seguinte maneira:

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

Exemplo completo

Veja a seguir um exemplo completo do plug-in de telemetria criado acima. Para Para um exemplo real, veja o 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;

Solução de problemas

Se você estiver com dificuldade para fazer com que os dados apareçam no lugar esperado, o OpenTelemetry oferece uma Ferramenta de diagnóstico que ajuda a localizar a origem do problema.