Open3

フロントエンド計装

Tomonori HayashiTomonori Hayashi

フロントエンドでも計装ができることを知ってから、自分でもやってみた話です。結論から言うと、それっぽいスパンはバックエンドツールで可視化されたけど、カスタムスパンなどは作成できませんでした。

ただ、コードの中身を見に行ったりと勉強になったのでまとめておこうと思います。

Next.js でサンプリアプリ

下記の 2 つのリポジトリを使っています。

Otel Collector 経由で Cloud Trace で可視化してみました。

計装してみる

手動計装

今回は src ディレクトリが存在するので、その直下に instrumentation.tsinstrumentation.node.ts の 2 つのファイルを作成します[1]。さらに next.config.mjs に追記します。

instrumentation.ts
instrumentation.ts
export async function register() {
    if (process.env.NEXT_RUNTIME === 'nodejs') {
        await import('./instrumentation.node')
    }
}
instrumentation.node.ts
instrumentation.ts
import { Resource } from "@opentelemetry/resources";
import {
    BatchSpanProcessor, ConsoleSpanExporter
} from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { NodeSDK } from '@opentelemetry/sdk-node';
import { W3CTraceContextPropagator } from "@opentelemetry/core";

const sdk = new NodeSDK({
    resource: new Resource({
        ["service.name"]: "front-sample",
        ["SEMRESATTRS_SERVICE_NAME"]: "service-test",
        ["SEMRESATTRS_SERVICE_VERSION"]: "1.0"
    }),
    spanProcessors: [
        new BatchSpanProcessor(new ConsoleSpanExporter()),
        new BatchSpanProcessor(new OTLPTraceExporter(
            {
                url: "http://localhost:4317/v1/traces"
            }
    ))],
    instrumentations: [
        new HttpInstrumentation()
    ],
    textMapPropagator: new W3CTraceContextPropagator(),
});
sdk.start();
next.config.mjs
next.config.,js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  experimental: {
    instrumentationHook: true
  },
};

export default nextConfig;

これだけで基本的なスパンは取得できるようになると思います。

脚注
  1. Next.js 公式ドキュメント ↩︎

Tomonori HayashiTomonori Hayashi

リポジトリを見ていく

opentelemetry/sdk-node

NodeSDK が何をやっているかを見る

import { NodeSDK } from '@opentelemetry/sdk-node';

リポジトリとしては experimental/packages/opentelemetry-sdk-node に存在する。

src/sdk.tsNodeSDK クラスの中の constructor の中で色々が行われている。

src/sdk.ts
src/sdk.ts
export class NodeSDK {
  private _tracerProviderConfig?: {
    tracerConfig: NodeTracerConfig;
    spanProcessors: SpanProcessor[];
    contextManager?: ContextManager;
    textMapPropagator?: TextMapPropagator;
  };
  ...

  /**
   * Create a new NodeJS SDK instance
   */
  public constructor(configuration: Partial<NodeSDKConfiguration> = {}) {
    const env = getEnv();
    const envWithoutDefaults = getEnvWithoutDefaults();

    ...

    this._configuration = configuration;

    this._resource = configuration.resource ?? new Resource({});

    ...

    this._resourceDetectors =
      configuration.resourceDetectors ?? defaultDetectors;

    this._serviceName = configuration.serviceName;

    this._autoDetectResources = configuration.autoDetectResources ?? true;

    // If a tracer provider can be created from manual configuration, create it
    if (
      configuration.traceExporter ||
      configuration.spanProcessor ||
      configuration.spanProcessors
    ) {
      const tracerProviderConfig: NodeTracerConfig = {};

      if (configuration.sampler) {
        tracerProviderConfig.sampler = configuration.sampler;
      }
      if (configuration.spanLimits) {
        tracerProviderConfig.spanLimits = configuration.spanLimits;
      }
      if (configuration.idGenerator) {
        tracerProviderConfig.idGenerator = configuration.idGenerator;
      }

      if (configuration.spanProcessor) {
        diag.warn(
          "The 'spanProcessor' option is deprecated. Please use 'spanProcessors' instead."
        );
      }

      const spanProcessor =
        configuration.spanProcessor ??
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        new BatchSpanProcessor(configuration.traceExporter!);

      const spanProcessors = configuration.spanProcessors ?? [spanProcessor];

      this._tracerProviderConfig = {
        tracerConfig: tracerProviderConfig,
        spanProcessors,
        contextManager: configuration.contextManager,
        textMapPropagator: configuration.textMapPropagator,
      };
    }
  }
  ...
  /**
   * Call this method to construct SDK components and register them with the OpenTelemetry API.
   */
  public start(): void {
    ...

    registerInstrumentations({
      instrumentations: this._instrumentations,
    });

    ...

    this._resource =
      this._serviceName === undefined
        ? this._resource
        : this._resource.merge(
            new Resource({
              [SEMRESATTRS_SERVICE_NAME]: this._serviceName,
            })
          );

    const Provider = this._tracerProviderConfig
      ? NodeTracerProvider
      : TracerProviderWithEnvExporters;

    const tracerProvider = new Provider({
      ...this._configuration,
      resource: this._resource,
    });

    this._tracerProvider = tracerProvider;

    if (this._tracerProviderConfig) {
      for (const spanProcessor of this._tracerProviderConfig.spanProcessors) {
        tracerProvider.addSpanProcessor(spanProcessor);
      }
    }

    tracerProvider.register({
      contextManager:
        this._tracerProviderConfig?.contextManager ??
        // _tracerProviderConfig may be undefined if trace-specific settings are not provided - fall back to raw config
        this._configuration?.contextManager,
      propagator: this._tracerProviderConfig?.textMapPropagator,
    });
    ...
    }
  }
 ....
}

constructor の中で resource , spanProcessor , traceExporter などが引数として渡されれば読み込まれる。

start()registerInstrumentations() を実行している。registerInstrumentations()experimental/packages/opentelemetry-instrumentation に存在する。

src/autoLoader.ts の function で定義されている。

src/autoLoader.ts
src/autoLoader.ts
import { trace, metrics } from '@opentelemetry/api';
import { logs } from '@opentelemetry/api-logs';
import {
  disableInstrumentations,
  enableInstrumentations,
} from './autoLoaderUtils';
import { AutoLoaderOptions } from './types_internal';

/**
 * It will register instrumentations and plugins
 * @param options
 * @return returns function to unload instrumentation and plugins that were
 *   registered
 */
export function registerInstrumentations(
  options: AutoLoaderOptions
): () => void {
  const tracerProvider = options.tracerProvider || trace.getTracerProvider();
  const meterProvider = options.meterProvider || metrics.getMeterProvider();
  const loggerProvider = options.loggerProvider || logs.getLoggerProvider();
  const instrumentations = options.instrumentations?.flat() ?? [];

  enableInstrumentations(
    instrumentations,
    tracerProvider,
    meterProvider,
    loggerProvider
  );
  ...
}

さらに enableInstrumentations() は、src/autoLoaderUtil.ts で function で定義されていて、各種 set 関数が実行されていて、 enable() も実行されている。

src/autoLoaderUtils.ts
src/autoLoaderUtils.ts
import { TracerProvider, MeterProvider } from '@opentelemetry/api';
import { Instrumentation } from './types';
import { LoggerProvider } from '@opentelemetry/api-logs';

/**
 * Enable instrumentations
 * @param instrumentations
 * @param tracerProvider
 * @param meterProvider
 */
export function enableInstrumentations(
  instrumentations: Instrumentation[],
  tracerProvider?: TracerProvider,
  meterProvider?: MeterProvider,
  loggerProvider?: LoggerProvider
): void {
  for (let i = 0, j = instrumentations.length; i < j; i++) {
    const instrumentation = instrumentations[i];
    if (tracerProvider) {
      instrumentation.setTracerProvider(tracerProvider);
    }
    if (meterProvider) {
      instrumentation.setMeterProvider(meterProvider);
    }
    if (loggerProvider && instrumentation.setLoggerProvider) {
      instrumentation.setLoggerProvider(loggerProvider);
    }
    // instrumentations have been already enabled during creation
    // so enable only if user prevented that by setting enabled to false
    // this is to prevent double enabling but when calling register all
    // instrumentations should be now enabled
    if (!instrumentation.getConfig().enabled) {
      instrumentation.enable();
    }
  }
}
Tomonori HayashiTomonori Hayashi

traceProvider について register()contextManagerpropagetor も引数に応じて設定される。

api の logsmetricssetGlobalhogehoge() してくれている。