feat: OTelのHTTPサーバ自動計装を追加 (#17711)

This commit is contained in:
おさむのひと
2026-07-14 18:06:25 +09:00
committed by GitHub
parent 91ea966b04
commit ecd85e5613
7 changed files with 151 additions and 11 deletions
@@ -32,6 +32,7 @@ import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
import { registerHttpServerInstrumentation } from './http-server-instrumentation.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -80,6 +81,7 @@ export class ServerService implements OnApplicationShutdown {
logger: false,
});
this.#fastify = fastify;
await registerHttpServerInstrumentation(fastify, this.config);
// HSTS
// 6months (15552000sec)
@@ -151,7 +151,7 @@ export class ApiCallService implements OnApplicationShutdown {
endpoint: IEndpoint & { exec: any },
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
reply: FastifyReply,
): void {
): Promise<void> {
const body = request.method === 'GET'
? request.query
: request.body;
@@ -162,10 +162,11 @@ export class ApiCallService implements OnApplicationShutdown {
: body?.['i'];
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
return Promise.resolve();
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, body, null, request).then((res) => {
return this.telemetryService.startSpan('API: ' + endpoint.name, () => this.authenticateService.authenticate(token).then(([user, app]) => {
const call = this.call(endpoint, user, app, body, null, request).then((res) => {
if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) {
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
@@ -177,9 +178,11 @@ export class ApiCallService implements OnApplicationShutdown {
if (user) {
this.logIp(request, user);
}
return call;
}).catch(err => {
this.#sendAuthenticationError(reply, err);
});
}));
}
@bindThis
@@ -222,8 +225,9 @@ export class ApiCallService implements OnApplicationShutdown {
reply.code(400);
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, fields, {
return await this.telemetryService.startSpan('API: ' + endpoint.name, () => this.authenticateService.authenticate(token).then(([user, app]) => {
const call = this.call(endpoint, user, app, fields, {
name: multipartData.filename,
path: path,
}, request).then((res) => {
@@ -235,9 +239,11 @@ export class ApiCallService implements OnApplicationShutdown {
if (user) {
this.logIp(request, user);
}
return call;
}).catch(err => {
this.#sendAuthenticationError(reply, err);
});
}));
}
@bindThis
@@ -431,9 +437,10 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
// API invoking
return await this.telemetryService.startSpan('API: ' + ep.name, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
// The API span starts in handleRequest/handleMultipartRequest so it also covers
// authentication, rate limiting, and parameter validation.
return await ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
}
@bindThis
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Config } from '@/config.js';
import type { FastifyInstance } from 'fastify';
type TelemetryConfig = Pick<Config, 'otelForBackend' | 'sentryForBackend'>;
export function shouldRegisterHttpServerInstrumentation(config: TelemetryConfig): boolean {
// Sentryもリクエストspanを作成するため、両方を登録すると重複して出力される。
return config.otelForBackend != null && config.sentryForBackend == null;
}
/**
* ActivityPubや
* well-knownを含む全HTTP受信経路を1つのroot spanとして計測する
*/
export async function registerHttpServerInstrumentation(fastify: FastifyInstance, config: TelemetryConfig): Promise<void> {
if (!shouldRegisterHttpServerInstrumentation(config)) return;
const { FastifyOtelInstrumentation } = await import('@fastify/otel');
const instrumentation = new FastifyOtelInstrumentation({
requestHook: (span, request) => {
const route = request.routeOptions.url;
if (route != null) {
// デフォルトだとトレース名が「request」で固定されてしまうため、判別がつかなくなる。
// ルート名をspan名に設定することで、トレースビューでルートごとの処理時間を確認できるようになる。
span.updateName(`${request.method} ${route}`);
}
},
});
await fastify.register(instrumentation.plugin());
}