Genkit テレメトリー プラグインの作成

OpenTelemetry は、トレース、指標、ログの収集をサポートしています。Node.js SDK を構成するテレメトリー プラグインを作成することで、Firebase Genkit を拡張して、すべてのテレメトリー データを任意の OpenTelemetry 対応システムにエクスポートできます。

設定

テレメトリーのエクスポートを制御するには、プラグインの PluginOptions は、Genkit の構成の telemetry ブロックに準拠する telemetry オブジェクトを提供する必要があります。

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

このオブジェクトは、次の 2 つの個別の構成を提供できます。

  • instrumentation: TracesMetrics の OpenTelemetry 構成を指定します。
  • logger: Genkit フローの入力と出力を含む構造化ログデータの書き込みに使用する、基盤となるロガーを提供します。

Node.js OpenTelemetry SDK のロギング機能は開発中であるため、現在この分離が必要となります。ロギングは個別に提供されるため、プラグインでデータの書き込み場所を明示的に制御できます。

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;

上記のコードブロックにより、プラグインはデベロッパーが使用できるテレメトリー構成を Genkit に提供します。

計測

トレースと指標のエクスポートを制御するには、プラグインで TelemetryConfig インターフェースに準拠する telemetry オブジェクトに instrumentation プロパティを指定する必要があります。

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

これにより、NodeSDK を起動するために Genkit フレームワークで使用される Partial<NodeSDKConfiguration> が提供されます。これにより、Genkit による OpenTelemetry 統合の使用方法をプラグインで完全に制御できます。

たとえば、次のテレメトリー構成は、シンプルなインメモリ トレースと指標エクスポータを提供します。

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

Genkit フレームワークが構造化ログデータを書き込むために使用するロガーを制御するには、プラグインで、LoggerConfig インターフェースに準拠する logger プロパティを telemetry オブジェクトに提供する必要があります。

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

一般的なロギング フレームワークのほとんどがこれに準拠しています。このようなフレームワークの 1 つに winston があります。これを使用すると、選択した場所にログデータを直接 push できるトランスポーターを構成できます。

たとえば、コンソールにログデータを書き込む winston ロガーを提供するには、プラグイン ロガーを次のように更新します。

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

ログとトレースのリンク

多くの場合、ログ ステートメントをプラグインによってエクスポートされた OpenTelemetry トレースに関連付けることをおすすめします。ログ ステートメントは OpenTelemetry フレームワークによって直接エクスポートされないため、追加の設定なしではエクスポートされません。幸いなことに、OpenTelemetry は、winstonpino などの一般的なロギング フレームワークのログ ステートメントにトレース ID とスパン ID をコピーする計測をサポートしています。@opentelemetry/auto-instrumentations-node パッケージを使用すると、これら(およびその他の)計測を自動的に構成できますが、トレースとスパンのフィールド名と値の制御が必要になることがあります。そのためには、TelemetryConfig で提供される NodeSDK 構成にカスタム LogHook インストルメンテーションを指定する必要があります。

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

この例では、OpenTelemetry NodeSDK のすべての自動計測を有効にし、カスタム WinstonInstrumentation を指定してトレース ID とスパン ID をログ メッセージのカスタム フィールドに書き込んでいます。

Genkit フレームワークでは、プラグインの LoggerConfig の前にプラグインの TelemetryConfig が初期化されることが保証されますが、LoggerConfig が初期化されるまで、基になるロガーがインポートされないように注意する必要があります。たとえば、上記の loggingConfig は次のように変更できます。

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

上記で作成したテレメトリー プラグインの完全な例を以下に示します。実際の例として、@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;

トラブルシューティング

目的の場所にデータが表示されない場合は、OpenTelemetry の診断ツールを使用して問題の原因を特定できます。