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. Basta gravar um plug-in de telemetria que configure o SDK do Node.js.

Configuração

Para controlar a exportação de telemetria, o PluginOptions do plug-in precisa fornecer um objeto telemetry que esteja 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 subjacente usado pelo Genkit para gravar dados de registro estruturados, incluindo entradas e saídas de fluxos do Genkit.

Atualmente, essa separação é necessária porque a funcionalidade de geração de registros para o SDK do OpenTelemetry do Node.js ainda está em desenvolvimento. O Logging é fornecido separadamente para que um plug-in possa controlar onde os dados são gravados 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 agora fornecerá ao Genkit uma configuração de telemetria que pode ser usada pelos desenvolvedores.

Instrumentação

Para controlar a exportação de traces e métricas, seu plug-in precisa fornecer uma 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 framework do Genkit para iniciar o NodeSDK. Isso dá ao plug-in controle total de como a integração do OpenTelemetry é usada pelo Genkit.

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

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;
}

Os frameworks de geração de registros mais conhecidos seguem esse modelo. Um desses frameworks é o 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 do winston que grava dados de registro no console, é possível atualizar o logger do plug-in 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 os rastros do OpenTelemetry exportados pelo plug-in. Como os log statements não são exportados diretamente pelo framework do OpenTelemetry, isso não acontece imediatamente. O OpenTelemetry oferece suporte a instrumentações que copiam IDs de trace e de período em log statements para frameworks de geração de registro conhecidos, como winston e pino (links em inglês). Ao usar o pacote @opentelemetry/auto-instrumentations-node, essas e outras instrumentações podem ser configuradas automaticamente. No entanto, em alguns casos, pode ser necessário controlar os nomes e valores de campos para traces e períodos. Para isso, é preciso fornecer uma instrumentação personalizada do LogHook à configuração do NodeSDK 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 o NodeSDK do OpenTelemetry e, em seguida, fornece um WinstonInstrumentation personalizado que grava os IDs de trace e de período em 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 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

Confira a seguir um exemplo completo do plug-in de telemetria criado acima. Para ver um exemplo real, dê uma olhada no 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 que os dados apareçam onde esperado, o OpenTelemetry fornece uma ferramenta de diagnóstico útil que ajuda a localizar a origem do problema.