フロントエンド計装
フロントエンドでも計装ができることを知ってから、自分でもやってみた話です。結論から言うと、それっぽいスパンはバックエンドツールで可視化されたけど、カスタムスパンなどは作成できませんでした。
ただ、コードの中身を見に行ったりと勉強になったのでまとめておこうと思います。
Next.js でサンプリアプリ
下記の 2 つのリポジトリを使っています。
Otel Collector 経由で Cloud Trace で可視化してみました。
計装してみる
手動計装
今回は src
ディレクトリが存在するので、その直下に instrumentation.ts
と instrumentation.node.ts
の 2 つのファイルを作成します[1]。さらに next.config.mjs
に追記します。
instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation.node')
}
}
instrumentation.node.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
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
instrumentationHook: true
},
};
export default nextConfig;
これだけで基本的なスパンは取得できるようになると思います。
リポジトリを見ていく
opentelemetry/sdk-node
NodeSDK が何をやっているかを見る
import { NodeSDK } from '@opentelemetry/sdk-node';
リポジトリとしては experimental/packages/opentelemetry-sdk-node
に存在する。
src/sdk.ts の NodeSDK
クラスの中の constructor
の中で色々が行われている。
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
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
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();
}
}
}
traceProvider について register()
で contextManager
や propagetor
も引数に応じて設定される。
api の logs
や metrics
は setGlobalhogehoge()
してくれている。