Pisanie wtyczki telemetrycznej Genkit

OpenTelemetry obsługuje zbieranie logów czasu, wskaźników i logów. Pakiet Firebase Genkit można rozszerzyć tak, aby eksportować wszystkie dane telemetryczne do dowolnego systemu obsługującego OpenTelemetry. Aby to zrobić, napisz wtyczkę do telemetrii, która konfiguruje pakiet SDK Node.js.

Konfiguracja

Aby można było kontrolować eksportowanie danych telemetrycznych, PluginOptions Twojej wtyczki musi udostępniać obiekt telemetry zgodny z blokiem telemetry w konfiguracji Genkit.

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

Ten obiekt może mieć 2 oddzielne konfiguracje:

  • instrumentation: udostępnia konfigurację OpenTelemetry dla Traces i Metrics.
  • logger: udostępnia podstawowy rejestrator używany przez Genkit do zapisywania uporządkowanych danych logów, w tym danych wejściowych i wyjściowych w przepływach Genkit.

Ten rozdzielenie jest obecnie konieczne, ponieważ funkcja logowania w pakiecie Node.js OpenTelemetry SDK jest nadal w trakcie opracowywania. Logowanie jest zapewniane oddzielnie, dzięki czemu wtyczka może kontrolować, gdzie są zapisywane dane.

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;

Powyższy blok kodu sprawi, że wtyczka będzie udostępniać w narzędziu Genkit narzędzie do konfiguracji danych telemetrycznych, z których mogą korzystać deweloperzy.

Instrumentacja

Aby kontrolować eksportowanie logów czasu i wskaźników, Twoja wtyczka musi udostępnić w obiekcie telemetry właściwość instrumentation zgodną z interfejsem TelemetryConfig:

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

Spowoduje to utworzenie Partial<NodeSDKConfiguration>, który będzie używany przez platformę Genkit do uruchomienia NodeSDK. Daje to wtyczce pełną kontrolę nad sposobem korzystania z integracji OpenTelemetry przez Genkit.

Na przykład opisana poniżej konfiguracja telemetrii zapewnia prosty log czasu w pamięci i eksporter wskaźników:

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

Rejestrator

Aby można było kontrolować rejestrator używany przez platformę Genkit do zapisywania uporządkowanych danych dziennika, wtyczka musi udostępniać w obiekcie telemetry właściwość logger zgodną z interfejsem LoggerConfig:

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

Spełnia to najpopularniejsze platformy logowania. Jedną z takich platform jest winston, która umożliwia skonfigurowanie przewoźników umożliwiających bezpośrednie przesyłanie danych logów do wybranej przez Ciebie lokalizacji.

Jeśli na przykład chcesz udostępnić narzędzie Winston Logger, które będzie zapisywać dane dziennika w konsoli, możesz zaktualizować rejestrator wtyczki, aby używał tego narzędzia:

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

Łączenie logów i logów czasu

Często warto, aby instrukcje logów były powiązane ze śladami OpenTelemetry wyeksportowanymi przez wtyczkę. Ponieważ instrukcje logów nie są eksportowane bezpośrednio przez platformę OpenTelemetry, nie dzieje się to od razu. Na szczęście OpenTelemetry obsługuje narzędzia, które kopiują identyfikatory logu czasu i spanów do instrukcji logu w popularnych platformach logowania, takich jak winston i pino. Korzystając z pakietu @opentelemetry/auto-instrumentations-node, możesz automatycznie skonfigurować te (i inne) instrumenty, ale w niektórych przypadkach może być konieczne kontrolowanie nazw i wartości pól w logach czasu oraz spanów. Aby to zrobić, musisz udostępnić niestandardową instrumentację LogHook do konfiguracji NodeSDK dostarczonej przez Twój 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;
      },
    }),
  ]);

W przykładzie włącza się wszystkie automatyczne instrumentacje w elemencie OpenTelemetry NodeSDK, a następnie udostępnia niestandardowy WinstonInstrumentation, który zapisuje identyfikatory logów czasu i spanów w polach niestandardowych w komunikacie logu.

Platforma Genkit gwarantuje, że TelemetryConfig wtyczki zostanie zainicjowane przed jej zdarzeniem LoggerConfig, ale musisz upewnić się, że bazowy rejestrator nie zostanie zaimportowany, dopóki nie zostanie zainicjowany LoggerConfig. Na przykład powyższą konfigurację LoggingConfig można zmodyfikować w następujący sposób:

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

Pełny przykład

Poniżej znajdziesz pełny przykład utworzonej powyżej wtyczki do telemetrii. Przyjrzyj się autentycznemu przykładowi wtyczki @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;

Rozwiązywanie problemów

Jeśli masz problemy z wyświetleniem danych w oczekiwanym miejscu, OpenTelemetry udostępnia przydatne narzędzie diagnostyczne, które pomaga zlokalizować źródło problemu.