כתיבת פלאגין של טלמטריה ב-Genkit

OpenTelemetry תומך באיסוף נתוני מעקב, מדדים ויומנים. אפשר להרחיב את Firebase Genkit כדי לייצא את כל נתוני הטלמטריה לכל מערכת שתומכת ב-OpenTelemetry. לשם כך, כותבים פלאגין טלמטריה שמגדיר את ה-SDK של Node.js.

תצורה

כדי לשלוט בייצוא הטלמטריה, ה-PluginOptions של הפלאגין צריך לספק אובייקט telemetry שתואם לבלוק telemetry בתצורה של Genkit.

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

האובייקט הזה יכול לספק שתי הגדרות נפרדות:

  • instrumentation: מספקת הגדרות אישיות של OpenTelemetry עבור Traces Metrics.
  • 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 נתוני טלמטריה לשימוש כללי, שיכול לשמש מפתחים.

אינסטרומנטציה

כדי לשלוט בייצוא של מעקבים ומדדים, הפלאגין צריך לספק המאפיין instrumentation באובייקט telemetry שתואם ממשק TelemetryConfig:

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

הפעולה הזו מספקת Partial<NodeSDKConfiguration> שישמש את של Genkit כדי להפעיל NodeSDK. כך הפלאגין יכול לשלוט באופן מלא באופן שבו 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),
      }),
    };
  },
};

יומן

כדי לשלוט ביומן שמשמש את מסגרת Genkit לכתיבת נתוני יומן מובנים, הפלאגין חייב לספק מאפיין logger באובייקט telemetry שתואם ממשק LoggerConfig:

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

קישור בין יומנים ו-Traces

בדרך כלל רצוי שהצהרות היומן יהיו בקורלציה עם עקבות OpenTelemetry שיוצאו על ידי הפלאגין. המערכת לא מייצאת את הצהרות היומן ישירות באמצעות מסגרת OpenTelemetry, ולכן זה לא קורה באופן אוטומטי. למרבה המזל, OpenTelemetry תומך בכלים שיעתיקו נתוני מעקב וגם להעביר מזהים להצהרות יומן עבור מסגרות רישום פופולריות, כמו winston ו-Pino. באמצעות חבילת @opentelemetry/auto-instrumentations-node, אפשר להגדיר את הכלים האלה (ואחרים) באופן אוטומטי, אבל במקרים מסוימים תצטרכו לשלוט בשמות ובערכים של השדות של נתוני מעקב, כוללת. כדי לעשות זאת, צריך לספק אינסטרומנטציה מותאמת אישית של LogHhook ההגדרות האישיות של Node SDK שסופקו על ידי 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;
      },
    }),
  ]);

בדוגמה הזו מפעילים את כל הכלים האוטומטיים של OpenTelemetry NodeSDK, ואז מספקים 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 מספק כלי אבחון שימושי שעוזר לאתר את מקור הבעיה.