Genkit 원격 분석 플러그인 작성

OpenTelemetry는 trace, 측정항목, 로그 수집을 지원합니다. Node.js SDK를 구성하는 원격 분석 플러그인을 작성하여 Firebase Genkit를 확장하여 모든 원격 분석 데이터를 OpenTelemetry 지원 시스템으로 내보낼 수 있습니다.

구성

원격 분석 내보내기를 제어하려면 플러그인의 PluginOptions에서 Genkit 구성의 telemetry 블록을 준수하는 telemetry 객체를 제공해야 합니다.

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

이 객체는 별도의 두 가지 구성을 제공할 수 있습니다.

  • instrumentation: TracesMetrics의 OpenTelemetry 구성을 제공합니다.
  • logger: Genkit 흐름의 입력과 출력을 포함하여 구조화된 로그 데이터를 작성하기 위해 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에 개발자가 사용할 수 있는 원격 분석 구성을 제공합니다.

계측

trace 및 측정항목 내보내기를 제어하려면 플러그인은 TelemetryConfig 인터페이스를 준수하는 telemetry 객체에 instrumentation 속성을 제공해야 합니다.

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

이는 Genkit 프레임워크에서 NodeSDK를 시작하는 데 사용할 Partial<NodeSDKConfiguration>를 제공합니다. 이를 통해 플러그인이 Genkit에서 OpenTelemetry 통합을 사용하는 방식을 완전히 제어할 수 있습니다.

예를 들어 다음 원격 분석 구성은 간단한 메모리 내 trace 및 측정항목 내보내기 도구를 제공합니다.

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 프레임워크에서 구조화된 로그 데이터를 작성하는 데 사용하는 로거를 제어하려면 플러그인이 telemetry 객체에 LoggerConfig 인터페이스를 준수하는 logger 속성을 제공해야 합니다.

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

가장 많이 사용되는 로깅 프레임워크가 이를 준수합니다. 이러한 프레임워크 중 하나가 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}`;
      }),
    });
  }
};

로그 및 Trace 연결

로그 구문과 플러그인이 내보낸 OpenTelemetry trace와 상관관계가 있는 것이 바람직한 경우가 많습니다. 로그 구문은 OpenTelemetry 프레임워크에서 직접 내보내지지 않으므로 처음부터 발생하지는 않습니다. 다행히 OpenTelemetry는 trace 및 스팬 ID를 winstonpino와 같이 널리 사용되는 로깅 프레임워크의 로그 구문에 복사하는 계측을 지원합니다. @opentelemetry/auto-instrumentations-node 패키지를 사용하면 이러한 계측 및 기타 계측을 자동으로 구성할 수 있지만 경우에 따라 trace 및 스팬의 필드 이름과 값을 제어해야 할 수도 있습니다. 이렇게 하려면 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의 모든 자동 계측을 사용 설정한 후 trace 및 스팬 ID를 로그 메시지의 커스텀 필드에 쓰는 커스텀 WinstonInstrumentation를 제공합니다.

Genkit 프레임워크는 플러그인의 TelemetryConfig가 플러그인의 LoggerConfig 전에 초기화되도록 보장하지만 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가 문제의 원인을 찾는 데 도움이 되는 유용한 진단 도구를 제공합니다.